@songsid/agend 2.2.0-alpha.9 → 2.2.0-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 +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +16 -9
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +147 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +3 -0
- package/dist/backend/antigravity.js +9 -0
- 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 +1 -1
- package/dist/backend/effort-metadata.js +3 -2
- 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-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +39 -3
- package/dist/backend/kiro.js +143 -19
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +33 -0
- package/dist/backend/launch-effort.js +123 -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 +33 -4
- package/dist/backend/types.js +2 -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 +98 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -2
- package/dist/channel/adapters/telegram.js +123 -19
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +24 -13
- 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.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +41 -3
- package/dist/cli.js +133 -28
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +6 -3
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +59 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +29 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +156 -1
- package/dist/daemon.js +764 -89
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +62 -7
- package/dist/delivery-outbox.js +309 -22
- 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/fleet-context.d.ts +10 -1
- package/dist/fleet-manager.d.ts +352 -10
- package/dist/fleet-manager.js +1925 -175
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- 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/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +29 -6
- package/dist/instance-lifecycle.js +79 -28
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +3 -1
- package/dist/instructions.js +11 -8
- package/dist/instructions.js.map +1 -1
- 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/locale.js +204 -44
- 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/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/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +263 -96
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +26 -15
- package/dist/outbound-schemas.js +24 -7
- package/dist/outbound-schemas.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.js +11 -4
- 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/reply-buttons-controller.d.ts +3 -0
- package/dist/reply-buttons-controller.js +3 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- 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 -0
- package/dist/restart-progress.js +30 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +46 -29
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +31 -0
- package/dist/service-installer.js +41 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +85 -25
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +72 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +28 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +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/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- 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 +4 -4
- package/dist/tips.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 +42 -2
- package/dist/topic-commands.js +92 -7
- 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 +80 -3
- package/dist/transcript-jsonl.js +147 -10
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +19 -0
- package/dist/transcript-monitor.js +87 -15
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +78 -0
- package/dist/transcript-sources.js +375 -64
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.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 +25 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +42 -2
- package/dist/ui/chat-store.js +2 -2
- package/dist/ui/chat-strings.js +38 -20
- package/dist/ui/chat-thread.js +69 -12
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/panel-chat.js +39 -10
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +199 -57
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +6 -2
- package/dist/ui/panel-settings.js +137 -32
- 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 +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +46 -4
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +43 -17
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +7 -2
- 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 +23 -5
- package/dist/ui/shared/app.css +159 -23
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +241 -45
- package/dist/ui/shared/instance-switch.js +4 -4
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +10 -1
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +32 -4
- package/dist/ui/signin.html +1 -1
- 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-install.d.ts +28 -2
- package/dist/update-install.js +22 -4
- package/dist/update-install.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/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- 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 +34 -5
- package/dist/usage/providers.js +160 -27
- 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.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +49 -2
- package/dist/web-api.js +421 -31
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +9 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-commands.d.ts +27 -4
- package/dist/web-commands.js +19 -4
- package/dist/web-commands.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- 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 +63 -0
- package/dist/web-prefs.js +265 -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-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 +36 -1
- package/dist/web-session.js +53 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +23 -0
- package/dist/web-settings-patch.js +77 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +5 -1
- package/dist/web-shell-routes.js +10 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/types.d.ts
CHANGED
|
@@ -247,6 +247,8 @@ export interface FleetDefaults extends Partial<InstanceConfig> {
|
|
|
247
247
|
* 0 turns the notice off. The owner reminder is not affected.
|
|
248
248
|
*/
|
|
249
249
|
reply_overdue_minutes?: number;
|
|
250
|
+
/** #1694: hours since the last ask before an open reply obligation expires (default 24; positive finite duration). */
|
|
251
|
+
reply_obligation_ttl_hours?: number;
|
|
250
252
|
scheduler?: {
|
|
251
253
|
max_schedules?: number;
|
|
252
254
|
default_timezone?: string;
|
|
@@ -343,6 +345,12 @@ export interface WebConfig {
|
|
|
343
345
|
echo_to_channel?: boolean;
|
|
344
346
|
/** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
|
|
345
347
|
usage_panel?: boolean;
|
|
348
|
+
/**
|
|
349
|
+
* #1218: the instance Log tab (紀錄) — each agent's steps (prompts, replies, tool calls and results, errors) read on
|
|
350
|
+
* demand from its own transcript, secrets masked. Default false: off, it does no work at all. Turning it on is an
|
|
351
|
+
* admin's change, confirmed like any access change (Settings → Web).
|
|
352
|
+
*/
|
|
353
|
+
log_tab?: boolean;
|
|
346
354
|
/** Expose provider API-key verify/apply endpoints (default false until dark-shipped). */
|
|
347
355
|
provider_secrets?: boolean;
|
|
348
356
|
/**
|
|
@@ -359,6 +367,17 @@ export interface WebConfig {
|
|
|
359
367
|
view_access?: "open" | "session";
|
|
360
368
|
/** Tell the admin channel when someone signs in to the web panel (default true). */
|
|
361
369
|
notify_login?: boolean;
|
|
370
|
+
/**
|
|
371
|
+
* #1620: how long a web session lasts. Only the LOCAL surface (the dashboard reached directly — loopback, a LAN, a
|
|
372
|
+
* tailnet) is configurable; a public-link session's limits stay fixed. Hours, 1 ≤ idle_hours ≤ absolute_hours ≤ 720;
|
|
373
|
+
* defaults 12 (absolute, from sign-in) and 2 (idle, since the last use). Shortening applies to existing sessions.
|
|
374
|
+
*/
|
|
375
|
+
session?: {
|
|
376
|
+
local?: {
|
|
377
|
+
absolute_hours?: number;
|
|
378
|
+
idle_hours?: number;
|
|
379
|
+
};
|
|
380
|
+
};
|
|
362
381
|
/**
|
|
363
382
|
* #1306: HTML previews in the web chat (default true). false: no preview listener; cards show Source and
|
|
364
383
|
* Download only. Even when true, a browser runs a preview only after that device opted in.
|
|
@@ -371,6 +390,12 @@ export interface WebConfig {
|
|
|
371
390
|
* previews through a tunnel or proxy need one. It must not be a name the dashboard answers to.
|
|
372
391
|
*/
|
|
373
392
|
preview_origin?: string;
|
|
393
|
+
/**
|
|
394
|
+
* #1723: previews over the temporary public link (default true): the link opens a second quick tunnel of its own
|
|
395
|
+
* for them, in the background, closed with the link. false: none on the public link. A web.preview_origin is used
|
|
396
|
+
* instead of it; web.preview: false turns previews off everywhere.
|
|
397
|
+
*/
|
|
398
|
+
preview_on_public_link?: boolean;
|
|
374
399
|
}
|
|
375
400
|
/** `login:` section — remote /login behaviour. */
|
|
376
401
|
export interface LoginConfig {
|
package/dist/ui/chat-commands.js
CHANGED
|
@@ -9,6 +9,7 @@ export const CHAT_COMMANDS = [
|
|
|
9
9
|
{ name: "compact", arg: "[instructions]" },
|
|
10
10
|
{ name: "clear", arg: "" },
|
|
11
11
|
{ name: "model", arg: "[name]" },
|
|
12
|
+
{ name: "backend", arg: "[backend] [model] [effort]" },
|
|
12
13
|
{ name: "effort", arg: "[level]" },
|
|
13
14
|
{ name: "cancel", arg: "" },
|
|
14
15
|
{ name: "btw", arg: "<question>" },
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// #1630 (design: the comment on #1630): the Discord server emoji the fleet's bots know — GET /ui/emojis, session-gated
|
|
2
|
+
// and served same-origin — so a message's `<:name:id>` / `<a:name:id>` can be drawn as its picture (chat-render.js).
|
|
3
|
+
// Read once per page, and only once a message shows such a tag; until it arrives (or if it fails) every tag stays text.
|
|
4
|
+
// A message drawn before the list arrived is drawn again when it does (the thread replaces what changed).
|
|
5
|
+
|
|
6
|
+
const TAG = /<a?:[A-Za-z0-9_]{2,32}:\d{15,25}>/;
|
|
7
|
+
const ID = /^\d{15,25}$/;
|
|
8
|
+
|
|
9
|
+
export function createEmojiIndex(fetchImpl) {
|
|
10
|
+
let ids = null; // Set of known ids once read (empty after a failure: no retry on this page)
|
|
11
|
+
let loading = false;
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const settle = (next) => { ids = next; for (const f of [...listeners]) f(); };
|
|
14
|
+
return {
|
|
15
|
+
known: (id) => !!ids && ids.has(id),
|
|
16
|
+
/** Any of these messages carries a server-emoji tag. */
|
|
17
|
+
wants: (msgs) => (msgs || []).some((x) => x && typeof x.text === "string" && TAG.test(x.text)),
|
|
18
|
+
/** Starts the one read of this page (once). */
|
|
19
|
+
ensure() {
|
|
20
|
+
if (ids || loading) return;
|
|
21
|
+
loading = true;
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
.then(() => fetchImpl("/ui/emojis"))
|
|
24
|
+
.then((r) => (r && r.ok ? r.json() : null))
|
|
25
|
+
.then((body) => {
|
|
26
|
+
const next = new Set();
|
|
27
|
+
if (body && Array.isArray(body.emojis)) for (const e of body.emojis) if (e && typeof e.id === "string" && ID.test(e.id)) next.add(e.id);
|
|
28
|
+
settle(next);
|
|
29
|
+
}, () => settle(new Set()));
|
|
30
|
+
},
|
|
31
|
+
subscribe(f) { listeners.add(f); return () => listeners.delete(f); },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// #1682 (design: the comment on #1682): the names of the Discord mentions a page shows — `<@id>` / `<@!id>` (a user),
|
|
2
|
+
// `<@&id>` (a role), `<#id>` (a channel). Asked from GET /ui/mentions (same-origin; the host answers from memory) only
|
|
3
|
+
// for the ids the page's messages carry, each id once per page, at most MAX_IDS per request. Until a name arrives (or
|
|
4
|
+
// if it never does) the pill reads @user / @role / #channel; when names arrive, the messages are drawn again.
|
|
5
|
+
|
|
6
|
+
const TOKEN = /<(@!?|@&|#)(\d{15,25})>/g;
|
|
7
|
+
const KIND = { "@": "u", "@!": "u", "@&": "r", "#": "c" };
|
|
8
|
+
const MAX_IDS = 100;
|
|
9
|
+
|
|
10
|
+
export function createMentionIndex(fetchImpl) {
|
|
11
|
+
const names = new Map(); // "u:id" → name
|
|
12
|
+
const asked = new Set(); // "u:id" asked already (answered or not: never again on this page)
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let inFlight = 0;
|
|
15
|
+
function idsIn(msgs) {
|
|
16
|
+
const want = [];
|
|
17
|
+
for (const x of msgs || []) {
|
|
18
|
+
if (!x || typeof x.text !== "string" || x.text.indexOf("<") === -1) continue;
|
|
19
|
+
for (const m of x.text.matchAll(TOKEN)) {
|
|
20
|
+
const key = `${KIND[m[1]]}:${m[2]}`;
|
|
21
|
+
if (!asked.has(key) && !want.includes(key)) want.push(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return want;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
/** The name the page knows for a mention, or null (the pill says @user / @role / #channel). */
|
|
28
|
+
name: (kind, id) => names.get(`${kind}:${id}`) ?? null,
|
|
29
|
+
/** Ask for the names of the mentions in these messages the page has not asked about yet. */
|
|
30
|
+
ensure(msgs) {
|
|
31
|
+
const want = idsIn(msgs);
|
|
32
|
+
for (let i = 0; i < want.length; i += MAX_IDS) {
|
|
33
|
+
const batch = want.slice(i, i + MAX_IDS);
|
|
34
|
+
for (const key of batch) asked.add(key);
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
for (const kind of ["u", "r", "c"]) {
|
|
37
|
+
const ids = batch.filter((k) => k[0] === kind).map((k) => k.slice(2));
|
|
38
|
+
if (ids.length) params.set(kind, ids.join(","));
|
|
39
|
+
}
|
|
40
|
+
inFlight++;
|
|
41
|
+
Promise.resolve()
|
|
42
|
+
.then(() => fetchImpl(`/ui/mentions?${params}`))
|
|
43
|
+
.then((r) => (r && r.ok ? r.json() : null))
|
|
44
|
+
.then((body) => {
|
|
45
|
+
let added = false;
|
|
46
|
+
for (const kind of ["u", "r", "c"]) {
|
|
47
|
+
const got = body && typeof body[kind] === "object" && body[kind] ? body[kind] : {};
|
|
48
|
+
for (const key of batch) {
|
|
49
|
+
if (key[0] !== kind) continue;
|
|
50
|
+
const id = key.slice(2);
|
|
51
|
+
if (Object.prototype.hasOwnProperty.call(got, id) && typeof got[id] === "string" && got[id]) { names.set(key, got[id]); added = true; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (added) for (const f of [...listeners]) f();
|
|
55
|
+
}, () => {})
|
|
56
|
+
.finally(() => { inFlight--; });
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
subscribe(f) { listeners.add(f); return () => listeners.delete(f); },
|
|
60
|
+
/** Requests not answered yet (tests). */
|
|
61
|
+
pending: () => inFlight,
|
|
62
|
+
};
|
|
63
|
+
}
|
package/dist/ui/chat-render.js
CHANGED
|
@@ -51,11 +51,44 @@
|
|
|
51
51
|
return s;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// #1630: a Discord server emoji, `<:name:id>` / `<a:name:id>` — escaped by now — drawn as its picture. Only an id the
|
|
55
|
+
// page knows (the fleet's servers: /ui/emojis) and only from the dashboard's own route; the src is built from the
|
|
56
|
+
// matched digits, the name (word characters only) goes into alt/title. An unknown id stays the literal text.
|
|
57
|
+
var EMOJI_RE = /<(a?):([A-Za-z0-9_]{2,32}):(\d{15,25})>/g;
|
|
58
|
+
function emojiImg(id, name) {
|
|
59
|
+
return '<img class="emoji" src="/ui/img/emoji/' + id + '" alt=":' + name + ':" title=":' + name + ':" decoding="async">';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// #1682: Discord mentions — `<@id>` / `<@!id>` (a user), `<@&id>` (a role), `<#id>` (a channel) — escaped by now (the
|
|
63
|
+
// role's & too), drawn as a pill: the name the page knows (store.mentions(kind, id), inserted escaped), else @user /
|
|
64
|
+
// @role / #channel; never the raw token. The id is the token's own digits (title, data-mention: the page fills a
|
|
65
|
+
// name in later). And a Telegram @username (a letter, then 4–31 word characters; not part of a word, an e-mail
|
|
66
|
+
// or a path) gets the same look — no lookup, no link. It runs after the links (see inline()).
|
|
67
|
+
var MENTION_RE = /<(@!?|@&|#)(\d{15,25})>/g;
|
|
68
|
+
// Not a package or a path either: `@scope/pkg`, `@anthropic-ai/sdk` (followed by - or /) stay text.
|
|
69
|
+
var TG_MENTION_RE = /(^|[^\w@\/.&;])@([A-Za-z][A-Za-z0-9_]{4,31})(?![\w@\/-])/g;
|
|
70
|
+
var MENTION_KIND = { "@": "u", "@!": "u", "@&": "r", "#": "c" };
|
|
71
|
+
var MENTION_UNKNOWN = { u: "@user", r: "@role", c: "#channel" };
|
|
72
|
+
function mentionPill(kind, id, name) {
|
|
73
|
+
var label = name ? (kind === "c" ? "#" : "@") + escapeHtml(name) : MENTION_UNKNOWN[kind];
|
|
74
|
+
return '<span class="mention" data-mention="' + kind + ":" + id + '" title="' + id + '">' + label + "</span>";
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
/** Inline Markdown over ALREADY-ESCAPED text. */
|
|
55
78
|
function inline(escaped, store) {
|
|
56
79
|
var s = escaped;
|
|
57
80
|
// `code` first: nothing inside it is Markdown.
|
|
58
|
-
|
|
81
|
+
// #1812: in a table cell a path may wrap after its separators — the cell keeps words whole, so without these a long
|
|
82
|
+
// path held its whole column wide.
|
|
83
|
+
s = s.replace(/`([^`\n]+)`/g, function (_m, code) { return stash(store, "<code>" + (store.inCell ? code.replace(/\//g, "/<wbr>") : code) + "</code>"); });
|
|
84
|
+
var known = store.emojis;
|
|
85
|
+
if (known) s = s.replace(EMOJI_RE, function (m, _a, name, id) { return known(id) ? stash(store, emojiImg(id, name)) : m; });
|
|
86
|
+
var names = store.mentions;
|
|
87
|
+
s = s.replace(MENTION_RE, function (_m, sigil, id) {
|
|
88
|
+
var kind = MENTION_KIND[sigil];
|
|
89
|
+
var name = names ? names(kind, id) : null;
|
|
90
|
+
return stash(store, mentionPill(kind, id, typeof name === "string" && name ? name : null));
|
|
91
|
+
});
|
|
59
92
|
// [label](url)
|
|
60
93
|
s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function (m, label, url) {
|
|
61
94
|
var href = safeHref(url);
|
|
@@ -66,6 +99,8 @@
|
|
|
66
99
|
var href = safeHref(url);
|
|
67
100
|
return href ? pre + stash(store, link(href, url)) : m;
|
|
68
101
|
});
|
|
102
|
+
// #1682: a Telegram @username — after the links (a link is stashed by now, so an @ inside a URL is never touched).
|
|
103
|
+
s = s.replace(TG_MENTION_RE, function (_m, pre, user) { return pre + stash(store, '<span class="mention tg">@' + user + "</span>"); });
|
|
69
104
|
return emphasis(s);
|
|
70
105
|
}
|
|
71
106
|
|
|
@@ -147,7 +182,8 @@
|
|
|
147
182
|
function renderTable(header, aligns, rows, store) {
|
|
148
183
|
function cell(tag, text, i) {
|
|
149
184
|
var a = aligns[i] ? ' class="al-' + aligns[i] + '"' : "";
|
|
150
|
-
|
|
185
|
+
store.inCell = true;
|
|
186
|
+
try { return "<" + tag + a + ">" + inline(text, store) + "</" + tag + ">"; } finally { store.inCell = false; }
|
|
151
187
|
}
|
|
152
188
|
function row(tag, cells) {
|
|
153
189
|
var out = "";
|
|
@@ -273,6 +309,10 @@
|
|
|
273
309
|
var cards = !!(opts && opts.htmlCards);
|
|
274
310
|
var src = String(text == null ? "" : text).replace(/\r\n?/g, "\n").replace(/\u0000/g, ""); // \u0000 is the placeholder mark
|
|
275
311
|
var store = [];
|
|
312
|
+
// #1630: which server emoji ids the page knows (a function id → boolean); without it every `<:name:id>` stays text.
|
|
313
|
+
store.emojis = opts && typeof opts.emojis === "function" ? opts.emojis : null;
|
|
314
|
+
// #1682: the names the page knows for mentions (a function (kind "u" | "r" | "c", id) → name or null).
|
|
315
|
+
store.mentions = opts && typeof opts.mentions === "function" ? opts.mentions : null;
|
|
276
316
|
var html = "";
|
|
277
317
|
var re = new RegExp(FENCE_RE.source, "g");
|
|
278
318
|
var last = 0;
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -66,7 +66,7 @@ export function createChatStore(deps) {
|
|
|
66
66
|
if (!m || typeof m.instance !== "string") return;
|
|
67
67
|
const before = s.msgs[m.instance];
|
|
68
68
|
s.msgs[m.instance] = R().mergeMessages(before, [m], 500);
|
|
69
|
-
if (m.instance === s.current && s.msgs[m.instance] !== before && !isUser(m, m.instance) && (s.msgs[m.instance] || []).length > (before || []).length) deps.announce(tf("chat.a11yReplied", m.sender));
|
|
69
|
+
if (m.instance === s.current && s.msgs[m.instance] !== before && m.role !== "status" && !isUser(m, m.instance) && (s.msgs[m.instance] || []).length > (before || []).length) deps.announce(tf("chat.a11yReplied", m.sender));
|
|
70
70
|
if (s.msgs[m.instance] !== before) changed(m.instance, "msgs");
|
|
71
71
|
}
|
|
72
72
|
function applyDeliveries(list) {
|
|
@@ -300,7 +300,7 @@ export function createChatStore(deps) {
|
|
|
300
300
|
};
|
|
301
301
|
let r = await post({});
|
|
302
302
|
if (r && r.confirm) {
|
|
303
|
-
const yes = deps.confirm ? await deps.confirm({ title: `/${command}`, message: r.confirm.message, confirmLabel: t("chat.cmdConfirm"), danger: true }) : false;
|
|
303
|
+
const yes = deps.confirm ? await deps.confirm({ title: `/${command}`, message: r.confirm.message, confirmLabel: t(command === "backend" ? "chat.cmdConfirmBackend" : "chat.cmdConfirm"), danger: true }) : false;
|
|
304
304
|
if (!yes || !alive()) { delete s.commands[target]; changed(target); return; }
|
|
305
305
|
r = await post({ confirm: r.confirm.token });
|
|
306
306
|
}
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -3,11 +3,11 @@ import { register } from "/assets/app-i18n.js";
|
|
|
3
3
|
|
|
4
4
|
register("chat", {
|
|
5
5
|
en: {
|
|
6
|
-
cmdConfirm: "Clear", cmdRunning: "Running /{0}…", cmdNotCommand: "Not a command here — Enter sends it as a message.", cmdDismiss: "Dismiss", cmdCommands: "Commands",
|
|
6
|
+
cmdConfirm: "Clear", cmdConfirmBackend: "Switch and restart", cmdRunning: "Running /{0}…", cmdNotCommand: "Not a command here — Enter sends it as a message.", cmdDismiss: "Dismiss", cmdCommands: "Commands",
|
|
7
7
|
cmdChoose: "Choose", cmdCurrent: "current", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
8
8
|
chipModel: "Model: {0} — choose another", chipEffort: "Effort: {0} — choose another",
|
|
9
9
|
cmd_ctx: "Context use, model and effort", cmd_compact: "Compact the conversation", cmd_clear: "Clear the conversation (asks first)",
|
|
10
|
-
cmd_model: "Show or switch the model", cmd_effort: "Show or set the reasoning effort", cmd_cancel: "Stop the current reply",
|
|
10
|
+
cmd_model: "Show or switch the model", cmd_backend: "Switch the CLI backend, then model and effort (restarts)", cmd_effort: "Show or set the reasoning effort", cmd_cancel: "Stop the current reply",
|
|
11
11
|
cmd_btw: "A side question that does not interrupt", cmd_steer: "Send into the current turn", cmd_pause: "Pause the instance",
|
|
12
12
|
cmd_wake: "Wake a paused instance", cmd_save: "Save the conversation to a file",
|
|
13
13
|
tourTitle: "Welcome to web chat", tourStep: "{0} of {1}", tourBack: "Back", tourNext: "Next", tourDone: "Got it", tourSkip: "Skip",
|
|
@@ -21,15 +21,15 @@ register("chat", {
|
|
|
21
21
|
dropHere: "Drop files to attach them", asText: "As text", pasteAttached: "A long paste ({0} characters) was attached as a text file", pasteKeptAsText: "No room for another attachment: the long paste stays as text",
|
|
22
22
|
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line · / for commands · Esc stops the reply", attachTitle: "Attach files (images, PDF, text — or paste / drop them)",
|
|
23
23
|
pvPreview: "Preview", pvStop: "Stop", pvDownload: "Download", pvMenu: "Preview options", pvHtml: "HTML",
|
|
24
|
-
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews
|
|
25
|
-
pvConfirm: "Allow HTML previews on
|
|
24
|
+
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews — they may be able to send data out", pvDisallow: "Stop allowing HTML previews",
|
|
25
|
+
pvConfirm: "Allow HTML previews?\n\nThis applies on every device where you are signed in. A preview runs the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out, and it can slow or freeze this tab. You still click Preview each time.",
|
|
26
26
|
pvNever: "Never preview HTML on this device", pvNeverUndo: "Let this device preview HTML again", pvStarting: "Starting the preview…",
|
|
27
27
|
pvChanged: "Preview stopped — the message changed. Preview again.", pvTruncated: "Truncated — ask the agent to send it as a .html file.",
|
|
28
28
|
pvLoading: "Reading the file…", pvAttOver: "This file is over 1 MiB; it is not previewed.",
|
|
29
29
|
pvAttGone: "This file is no longer available here (it changed, or the fleet restarted). Ask the agent to send it again.",
|
|
30
30
|
pvAttFailed: "Could not read this file. Try again.",
|
|
31
31
|
pvBannerAck: "I understand", pvBannerAckTitle: "Hide this notice until you sign in again",
|
|
32
|
-
attGone: "This file could not be found again after AgEnD restarted (moved, deleted or changed). Ask the agent to send it again.", attUnavailable: "unavailable",
|
|
32
|
+
attGone: "This file could not be found again after AgEnD restarted (moved, deleted or changed). Ask the agent to send it again.", attUnavailable: "unavailable", yesterdayAt: "Yesterday {0}",
|
|
33
33
|
tcShow: "Show", tcHide: "Hide", tcDownload: "Download", tcShowAll: "Show all {0} lines", tcOver: "This file is over 1 MiB; download it to read it.", tcPlain: "Too large to format here: shown as plain text.",
|
|
34
34
|
rbGroup: "Choices", rbChosen: "Chosen by {0}", rbExpired: "Expired — reply in text",
|
|
35
35
|
pvOpenPanel: "Open in panel", pvInPanel: "Shown in the panel.", pvPanel: "HTML preview", pvSource: "From {0}, {1} — show the message",
|
|
@@ -37,8 +37,8 @@ register("chat", {
|
|
|
37
37
|
pvNewer: "A newer version is in the reply from {0}.", pvNewerShow: "Show it", pvPanelIdle: "Click Preview to run it here.",
|
|
38
38
|
lbLabel: "Image: {0}", lbLabelNoName: "Image", lbClose: "Close", lbOpenOriginal: "Open original", lbPrev: "Previous image", lbNext: "Next image", lbCount: "{0} / {1}",
|
|
39
39
|
// #1554: preview.js's words (its English is the fallback there), and the server's reasons by code.
|
|
40
|
-
pvWhyOptin: "HTML previews are off
|
|
41
|
-
pvWhyOptinPanel: "HTML previews are off
|
|
40
|
+
pvWhyOptin: "HTML previews are off. Allow them from this card's ⋯ menu or Settings → Preferences.",
|
|
41
|
+
pvWhyOptinPanel: "HTML previews are off.",
|
|
42
42
|
pvWhyNever: "Previews are switched off on this device for this session.",
|
|
43
43
|
pvWhyOrigin: "Previews are off: this page is at {0}, but the fleet believes it is at {1} (a proxy that rewrites Host). Pass the external Host through, or set web.preview_origin.",
|
|
44
44
|
pvUnknownAddress: "an unknown address", pvWhyServer: "Previews are not available here.",
|
|
@@ -53,10 +53,19 @@ register("chat", {
|
|
|
53
53
|
pvServer_needOrigin: "Previews need web.preview_origin when the dashboard is reached through a tunnel or proxy.",
|
|
54
54
|
pvServer_needHttps: "Previews need an https web.preview_origin when the dashboard is reached over https.",
|
|
55
55
|
pvServer_sameOrigin: "The preview origin cannot be the dashboard's own.",
|
|
56
|
-
pvServer_publicLink: "Previews are
|
|
56
|
+
pvServer_publicLink: "Previews are off on the public link (web.preview_on_public_link: false). Open the dashboard on the machine running AgEnD to preview.",
|
|
57
|
+
pvServer_publicLinkStarting: "Preview is starting… — the public link's preview address is being opened; the preview runs here as soon as it is up.",
|
|
58
|
+
pvServer_publicLinkPreviewFailed: "Previews are unavailable on this public link: {0}. The link itself keeps working.",
|
|
59
|
+
pvFail_exited: "its address closed unexpectedly", "pvFail_spawn-failed": "cloudflared could not start for it",
|
|
60
|
+
"pvFail_no-address": "no address was given for it", pvFail_unreachable: "its address could not be reached",
|
|
61
|
+
"pvFail_lease-held": "an earlier preview tunnel is still being cleaned up", "pvFail_gateway-failed": "its local listener could not start",
|
|
62
|
+
"pvFail_rate-limited": "Cloudflare is limiting new temporary links from this machine — try again in a while",
|
|
63
|
+
"pvFail_provisioning-failed": "Cloudflare refused to create its address right now", "pvFail_process-exited": "cloudflared stopped before it gave an address",
|
|
64
|
+
"pvFail_bad-address": "Cloudflare gave an address that could not be used",
|
|
65
|
+
pvFail_unknown: "an unexpected error",
|
|
57
66
|
pvServer_notOffered: "Previews are not available on this page.",
|
|
58
|
-
previews: "Allow HTML previews
|
|
59
|
-
copy: "Copy", copyMessage: "Copy this reply", copied: "Copied", copyFailed: "Copy failed", wrap: "Wrap", showAll: "Show all {0} lines", showLess: "Show less", jumpLatest: "Latest", newBelow: "{0} new",
|
|
67
|
+
previews: "Allow HTML previews",
|
|
68
|
+
copy: "Copy", copyMessage: "Copy this reply", copied: "Copied", copyFailed: "Copy failed", wrap: "Wrap", showAll: "Show all {0} lines", showMore: "Show more", showLess: "Show less", jumpLatest: "Latest", newBelow: "{0} new",
|
|
60
69
|
disconnected: "disconnected", tick_queued: "Waiting behind another message", tick_processing: "Handed to the agent", tick_delivered: "The agent has it", tick_failed: "Not delivered", tick_cancelled: "Dropped by Stop",
|
|
61
70
|
promptAnswering: "Answering…", promptGone: "This prompt is no longer open", promptFor: "{0}: {1}",
|
|
62
71
|
working: "{0} is working…", stuck: "{0} looks stuck", stopReply: "Stop reply", stopReplyTitle: "Stop the current reply (like Esc) and drop the messages still waiting. The instance keeps running.", stopSent: "Stopped",
|
|
@@ -78,11 +87,11 @@ register("chat", {
|
|
|
78
87
|
configured: "(configured)", fleetDefault: "(fleet default)",
|
|
79
88
|
},
|
|
80
89
|
"zh-TW": {
|
|
81
|
-
cmdConfirm: "清空", cmdRunning: "執行 /{0} 中…", cmdNotCommand: "這裡沒有這個指令——按 Enter 會當成一般訊息送出。", cmdDismiss: "關閉", cmdCommands: "指令",
|
|
90
|
+
cmdConfirm: "清空", cmdConfirmBackend: "切換並重新啟動", cmdRunning: "執行 /{0} 中…", cmdNotCommand: "這裡沒有這個指令——按 Enter 會當成一般訊息送出。", cmdDismiss: "關閉", cmdCommands: "指令",
|
|
82
91
|
cmdChoose: "選擇", cmdCurrent: "目前", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
83
92
|
chipModel: "模型:{0}——選擇其他", chipEffort: "推理強度:{0}——選擇其他",
|
|
84
93
|
cmd_ctx: "context 用量、模型與推理強度", cmd_compact: "壓縮對話", cmd_clear: "清空對話(會先確認)",
|
|
85
|
-
cmd_model: "查看或切換模型", cmd_effort: "查看或設定推理強度", cmd_cancel: "停止目前的回覆",
|
|
94
|
+
cmd_model: "查看或切換模型", cmd_backend: "切換 CLI backend,再選模型和推理強度(會重新啟動)", cmd_effort: "查看或設定推理強度", cmd_cancel: "停止目前的回覆",
|
|
86
95
|
cmd_btw: "不打斷目前工作的旁問", cmd_steer: "送進目前這一輪", cmd_pause: "暫停 instance",
|
|
87
96
|
cmd_wake: "喚醒已暫停的 instance", cmd_save: "把對話存成檔案",
|
|
88
97
|
tourTitle: "歡迎使用網頁聊天", tourStep: "{0} / {1}", tourBack: "上一步", tourNext: "下一步", tourDone: "知道了", tourSkip: "略過",
|
|
@@ -96,23 +105,23 @@ register("chat", {
|
|
|
96
105
|
dropHere: "放開即可附加檔案", asText: "改為文字", pasteAttached: "貼上的長文字({0} 個字元)已改為文字檔附加", pasteKeptAsText: "附件已滿,貼上的長文字保留為一般文字",
|
|
97
106
|
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行 · / 叫出指令 · Esc 中止回覆", attachTitle: "附加檔案(圖片、PDF、文字檔,也可以貼上或拖曳)",
|
|
98
107
|
pvPreview: "預覽", pvStop: "停止", pvDownload: "下載", pvMenu: "預覽選項", pvHtml: "HTML",
|
|
99
|
-
pvAllowButton: "允許預覽", pvAllow: "
|
|
100
|
-
pvConfirm: "
|
|
108
|
+
pvAllowButton: "允許預覽", pvAllow: "允許預覽 HTML(預覽可能把資料傳出去)", pvDisallow: "不再允許預覽 HTML",
|
|
109
|
+
pvConfirm: "要允許預覽 HTML 嗎?\n\n這會套用到你登入的每一台裝置。預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
|
101
110
|
pvNever: "這台裝置永不預覽 HTML", pvNeverUndo: "讓這台裝置再次可以預覽 HTML", pvStarting: "正在啟動預覽…",
|
|
102
111
|
pvChanged: "預覽已停止——訊息內容變了。請再預覽一次。", pvTruncated: "內容被截斷——請 agent 改用 .html 檔案傳送。",
|
|
103
112
|
pvLoading: "正在讀取檔案…", pvAttOver: "這個檔案超過 1 MiB,不提供預覽。",
|
|
104
113
|
pvAttGone: "這裡已無法取得這個檔案(檔案已變更,或艦隊已重新啟動)。請 agent 再傳一次。",
|
|
105
114
|
pvAttFailed: "無法讀取這個檔案,請再試一次。",
|
|
106
115
|
pvBannerAck: "我知道了", pvBannerAckTitle: "隱藏這則提醒,直到下次登入",
|
|
107
|
-
attGone: "AgEnD 重新啟動後找不到這個檔案(已移動、刪除或變更)。請 agent 再傳一次。", attUnavailable: "無法取得",
|
|
116
|
+
attGone: "AgEnD 重新啟動後找不到這個檔案(已移動、刪除或變更)。請 agent 再傳一次。", attUnavailable: "無法取得", yesterdayAt: "昨天 {0}",
|
|
108
117
|
tcShow: "顯示", tcHide: "收起", tcDownload: "下載", tcShowAll: "顯示全部 {0} 行", tcOver: "這個檔案超過 1 MiB,請下載後閱讀。", tcPlain: "內容太大,無法在這裡排版,改以純文字顯示。",
|
|
109
118
|
rbGroup: "選項", rbChosen: "{0} 已選擇", rbExpired: "已逾時,請直接回覆文字",
|
|
110
119
|
pvOpenPanel: "在面板中開啟", pvInPanel: "已在面板中顯示。", pvPanel: "HTML 預覽", pvSource: "來自 {0},{1}——顯示該訊息",
|
|
111
120
|
pvPage: "頁面", pvCode: "程式碼", pvReload: "重新載入", pvClose: "關閉面板", pvBack: "回到對話", pvResize: "調整預覽面板寬度",
|
|
112
121
|
pvNewer: "{0} 的回覆裡有較新的版本。", pvNewerShow: "顯示", pvPanelIdle: "按「預覽」在這裡執行。",
|
|
113
122
|
lbLabel: "圖片:{0}", lbLabelNoName: "圖片", lbClose: "關閉", lbOpenOriginal: "開啟原圖", lbPrev: "上一張", lbNext: "下一張", lbCount: "{0} / {1}",
|
|
114
|
-
pvWhyOptin: "
|
|
115
|
-
pvWhyOptinPanel: "
|
|
123
|
+
pvWhyOptin: "還沒允許 HTML 預覽。可從這張卡片的 ⋯ 選單或「設定 → 偏好設定」允許。",
|
|
124
|
+
pvWhyOptinPanel: "還沒允許 HTML 預覽。",
|
|
116
125
|
pvWhyNever: "這台裝置在這個工作階段已關閉預覽。",
|
|
117
126
|
pvWhyOrigin: "預覽已關閉:這個頁面位於 {0},但艦隊認為它位於 {1}(有會改寫 Host 的代理)。請讓外部 Host 原樣傳入,或設定 web.preview_origin。",
|
|
118
127
|
pvUnknownAddress: "未知的網址", pvWhyServer: "這裡無法使用預覽。",
|
|
@@ -127,10 +136,19 @@ register("chat", {
|
|
|
127
136
|
pvServer_needOrigin: "透過通道或代理開啟儀表板時,預覽需要設定 web.preview_origin。",
|
|
128
137
|
pvServer_needHttps: "以 https 開啟儀表板時,預覽需要 https 的 web.preview_origin。",
|
|
129
138
|
pvServer_sameOrigin: "預覽的來源不能和儀表板相同。",
|
|
130
|
-
pvServer_publicLink: "
|
|
139
|
+
pvServer_publicLink: "公開連結的預覽已關閉(web.preview_on_public_link: false)。要預覽,請在執行 AgEnD 的那台機器上開啟儀表板。",
|
|
140
|
+
pvServer_publicLinkStarting: "預覽啟動中…——正在開啟公開連結的預覽網址,好了就會在這裡執行。",
|
|
141
|
+
pvServer_publicLinkPreviewFailed: "這個公開連結無法預覽:{0}。連結本身照常可用。",
|
|
142
|
+
pvFail_exited: "它的網址意外關閉了", "pvFail_spawn-failed": "無法為它啟動 cloudflared",
|
|
143
|
+
"pvFail_no-address": "沒有取得它的網址", pvFail_unreachable: "連不到它的網址",
|
|
144
|
+
"pvFail_lease-held": "上一個預覽通道還在清理中", "pvFail_gateway-failed": "它的本機監聽無法啟動",
|
|
145
|
+
"pvFail_rate-limited": "Cloudflare 正在限制這台機器建立新的臨時連結 — 請過一陣子再試",
|
|
146
|
+
"pvFail_provisioning-failed": "Cloudflare 目前拒絕建立它的網址", "pvFail_process-exited": "cloudflared 在給出網址前就停止了",
|
|
147
|
+
"pvFail_bad-address": "Cloudflare 給出的網址無法使用",
|
|
148
|
+
pvFail_unknown: "發生了未預期的錯誤",
|
|
131
149
|
pvServer_notOffered: "這個頁面不提供預覽。",
|
|
132
|
-
previews: "
|
|
133
|
-
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
150
|
+
previews: "允許預覽 HTML",
|
|
151
|
+
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showMore: "顯示更多", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
134
152
|
disconnected: "已斷線", tick_queued: "排在前一則訊息之後", tick_processing: "已交給 agent", tick_delivered: "agent 已收到", tick_failed: "未送達", tick_cancelled: "已被中止取消",
|
|
135
153
|
promptAnswering: "回覆中…", promptGone: "這個提示已經結束", promptFor: "{0}:{1}",
|
|
136
154
|
working: "{0} 處理中…", stuck: "{0} 似乎卡住了", stopReply: "中止回覆", stopReplyTitle: "中止目前的回覆(同 Esc),並取消還在排隊的訊息。instance 會繼續運行。", stopSent: "已中止",
|
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,6 +23,9 @@ 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. */
|
|
@@ -31,7 +35,7 @@ export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
|
31
35
|
/**
|
|
32
36
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
33
37
|
* 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".
|
|
38
|
+
* key the side panel shows, or null; open(spec, { run }); version(); claim(); claims() } — without it a card has no "Open in panel".
|
|
35
39
|
*/
|
|
36
40
|
export function createThread(list, scroller, opts) {
|
|
37
41
|
const nodes = new Map(); // key → { html, node }
|
|
@@ -46,17 +50,48 @@ export function createThread(list, scroller, opts) {
|
|
|
46
50
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
47
51
|
const tr = opts.t, trf = opts.tf;
|
|
48
52
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
53
|
+
/** #1770: write a message node's time label for now; unchanged text is not touched. */
|
|
54
|
+
function timeLabel(node) {
|
|
55
|
+
const el = node.querySelector(".meta .time[data-ts]");
|
|
56
|
+
if (!el) return;
|
|
57
|
+
const v = chatTime(el.dataset.ts, { yesterday: (hm) => trf("chat.yesterdayAt", hm) });
|
|
58
|
+
if (el.textContent !== v) el.textContent = v;
|
|
59
|
+
}
|
|
60
|
+
// #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
|
|
61
|
+
let offDay = null;
|
|
62
|
+
function armDay() {
|
|
63
|
+
if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
|
|
64
|
+
else if (!nodes.size && offDay) { offDay(); offDay = null; }
|
|
65
|
+
}
|
|
49
66
|
|
|
50
67
|
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
68
|
|
|
52
69
|
function msgHtml(x) {
|
|
70
|
+
// #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
|
|
71
|
+
// is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
|
|
72
|
+
const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
|
|
73
|
+
if (x.role === "status") return statusHtml(x, time);
|
|
53
74
|
const u = opts.isUser(x);
|
|
54
|
-
const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
55
75
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
56
76
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
57
77
|
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
78
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
59
|
-
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span
|
|
79
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body"><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>`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
83
|
+
* a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
|
|
84
|
+
* start with "agend". A long one is folded until it is opened.
|
|
85
|
+
*/
|
|
86
|
+
function statusHtml(x, time) {
|
|
87
|
+
const text = String(x.text == null ? "" : x.text);
|
|
88
|
+
const lines = R().lineCount(text);
|
|
89
|
+
const manyLines = lines > STATUS_FOLD_LINES;
|
|
90
|
+
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
91
|
+
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
92
|
+
: "";
|
|
93
|
+
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>`;
|
|
94
|
+
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
95
|
}
|
|
61
96
|
/**
|
|
62
97
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -78,6 +113,7 @@ export function createThread(list, scroller, opts) {
|
|
|
78
113
|
tpl.innerHTML = html;
|
|
79
114
|
const node = tpl.content.firstElementChild;
|
|
80
115
|
decorateCode(node);
|
|
116
|
+
timeLabel(node);
|
|
81
117
|
if (x) decorateHtmlCards(node, x, stoppedKeys || []);
|
|
82
118
|
if (x) decorateTextCards(node, x);
|
|
83
119
|
return node;
|
|
@@ -299,14 +335,32 @@ export function createThread(list, scroller, opts) {
|
|
|
299
335
|
};
|
|
300
336
|
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
301
337
|
// Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
|
|
302
|
-
|
|
338
|
+
// A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
|
|
339
|
+
// own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
|
|
340
|
+
// Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
|
|
341
|
+
run.onclick = () => {
|
|
342
|
+
if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
|
|
343
|
+
const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
|
|
344
|
+
withCode((c) => {
|
|
345
|
+
if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
|
|
346
|
+
P().start(key, holder, c, ui);
|
|
347
|
+
});
|
|
348
|
+
};
|
|
303
349
|
stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
|
|
304
|
-
//
|
|
305
|
-
//
|
|
350
|
+
// #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
|
|
351
|
+
// starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
|
|
352
|
+
// previews is offered Allow first (and allowing runs it); where previews are off, nothing runs.
|
|
353
|
+
// An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
|
|
354
|
+
// wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
|
|
355
|
+
// a late read never undoes a newer click.
|
|
306
356
|
if (toPanel) toPanel.onclick = () => {
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
357
|
+
if (reading) return; // this card already reading: that click stays the one
|
|
358
|
+
if (P().running(key)) P().stop(key, "stopped", "");
|
|
359
|
+
const at = panel.claim(), started = P().startCount();
|
|
360
|
+
withCode((c) => {
|
|
361
|
+
if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
|
|
362
|
+
panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: true });
|
|
363
|
+
});
|
|
310
364
|
};
|
|
311
365
|
if (dl) dl.onclick = () => opts.download(code);
|
|
312
366
|
ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
|
|
@@ -341,6 +395,7 @@ export function createThread(list, scroller, opts) {
|
|
|
341
395
|
if (!msgs.length) {
|
|
342
396
|
for (const v of nodes.values()) stopIn(v.node);
|
|
343
397
|
nodes.clear();
|
|
398
|
+
armDay();
|
|
344
399
|
list.textContent = "";
|
|
345
400
|
opts.onEmpty(true);
|
|
346
401
|
return;
|
|
@@ -357,12 +412,13 @@ export function createThread(list, scroller, opts) {
|
|
|
357
412
|
const node = msgNode(html, x, stopped);
|
|
358
413
|
if (have) have.node.replaceWith(node); else added++;
|
|
359
414
|
have = { html, node }; nodes.set(key, have);
|
|
360
|
-
}
|
|
415
|
+
} else timeLabel(have.node);
|
|
361
416
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
362
417
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
363
418
|
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
364
419
|
prev = have.node;
|
|
365
420
|
}
|
|
421
|
+
armDay();
|
|
366
422
|
if ("restore" in renderOpts) {
|
|
367
423
|
unseen = 0;
|
|
368
424
|
if (renderOpts.restore == null) toBottom();
|
|
@@ -426,9 +482,9 @@ export function createThread(list, scroller, opts) {
|
|
|
426
482
|
if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
|
|
427
483
|
else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
|
|
428
484
|
else if (act === "toggleFold") {
|
|
429
|
-
const box = el.closest(".codeblock"); if (!box) return;
|
|
485
|
+
const box = el.closest(".codeblock, .msg.status-line"); if (!box) return;
|
|
430
486
|
const folded = box.classList.toggle("folded");
|
|
431
|
-
el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.
|
|
487
|
+
el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
|
|
432
488
|
} else if (act === "toggleWrap") opts.toggleWrap();
|
|
433
489
|
else if (act === "replyButton") {
|
|
434
490
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
@@ -441,6 +497,7 @@ export function createThread(list, scroller, opts) {
|
|
|
441
497
|
if (ro) ro.disconnect();
|
|
442
498
|
if (P()) P().stopAll("leave");
|
|
443
499
|
nodes.clear();
|
|
500
|
+
armDay();
|
|
444
501
|
}
|
|
445
502
|
|
|
446
503
|
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
|