@songsid/agend 2.1.12-beta.9 → 2.2.0-alpha.1
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 +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +83 -7
- package/dist/backend/kiro.js +418 -91
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +45 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +225 -127
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +89 -4
- package/dist/daemon.js +661 -77
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +6 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +293 -12
- package/dist/fleet-manager.js +2364 -449
- package/dist/fleet-manager.js.map +1 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +150 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +6 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +38 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +110 -0
- package/dist/service-activation.js +302 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +7 -0
- package/dist/service-installer.js +7 -4
- package/dist/service-installer.js.map +1 -1
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +175 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +133 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +197 -32
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +3 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +35 -5
- package/dist/tmux-control.js +291 -57
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +191 -58
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +289 -0
- package/dist/ui/chat-strings.js +69 -0
- package/dist/ui/chat-thread.js +209 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +418 -0
- package/dist/ui/panel-fleet.js +413 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +750 -0
- package/dist/ui/preview.js +266 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +652 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +344 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +676 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +130 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +61 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +51 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +71 -0
- package/dist/update-install.js +162 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +71 -14
- package/dist/web-api.js +492 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/package.json +8 -5
- package/scripts/preinstall-guard.cjs +36 -0
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// #1408 §5: the web chat's state, for the life of the page — every instance's messages, drafts, files waiting to go,
|
|
2
|
+
// failed sends, prompts and turn state. It is fed by the app's one stream from the first frame, whatever panel is on
|
|
3
|
+
// screen, so leaving the chat and coming back loses nothing (draft, scroll, files) and re-reads nothing.
|
|
4
|
+
//
|
|
5
|
+
// What it does is what the dashboard did (#1307, #1306, #1313, #1316, #1317); only where it lives changed. No DOM:
|
|
6
|
+
// the chat panel renders it; tests drive it directly.
|
|
7
|
+
//
|
|
8
|
+
// Requests and #1374: the history of an instance is read once per page, when a person first opens its chat (a
|
|
9
|
+
// navigation — it counts as use, as the old click did). Coming back to it reads nothing; the stream keeps it current.
|
|
10
|
+
import "./chat-render.js";
|
|
11
|
+
|
|
12
|
+
const R = () => globalThis.AgendChatRender;
|
|
13
|
+
|
|
14
|
+
/** Build a store. `deps` (fetch, toast, announce, t, now) are injected so tests can drive every path. */
|
|
15
|
+
export function createChatStore(deps) {
|
|
16
|
+
const s = {
|
|
17
|
+
msgs: {}, drafts: {}, failedSends: {}, pendingFiles: {}, sending: {}, inFlightFiles: {},
|
|
18
|
+
prompts: {}, workingSince: {}, stopping: {}, cancelling: {}, scrollMemo: {}, historyRead: new Set(),
|
|
19
|
+
exec: {}, awaiting: {}, current: null,
|
|
20
|
+
};
|
|
21
|
+
const subs = new Set();
|
|
22
|
+
const pastedTexts = new WeakMap();
|
|
23
|
+
const now = deps.now || (() => (typeof performance !== "undefined" ? performance.now() : Date.now()));
|
|
24
|
+
const t = deps.t, tf = deps.t;
|
|
25
|
+
|
|
26
|
+
/** Tell subscribers what changed: `kind` "msgs" (the thread redraws) or "state" (everything else). */
|
|
27
|
+
function changed(instance, kind = "state") { for (const fn of [...subs]) fn(instance, kind); }
|
|
28
|
+
async function api(method, path, body) {
|
|
29
|
+
const o = { method, headers: { "Content-Type": "application/json" } };
|
|
30
|
+
if (body) o.body = JSON.stringify(body);
|
|
31
|
+
const r = await deps.fetch(path, o);
|
|
32
|
+
return r.json();
|
|
33
|
+
}
|
|
34
|
+
const isUser = (x, instance) => x.sender === "web-user" || (!x.sender.startsWith("agend") && x.sender !== instance && x.sender !== "general");
|
|
35
|
+
|
|
36
|
+
// ── The turn (#1307): when this page saw the agent start working (its own monotonic clock, so approximate), a Stop
|
|
37
|
+
// on its way, and an instance waiting on a terminal prompt.
|
|
38
|
+
function noteExec(name, state) {
|
|
39
|
+
const was = R().isBusy(s.exec[name]), busy = R().isBusy(state);
|
|
40
|
+
const known = Object.prototype.hasOwnProperty.call(s.exec, name);
|
|
41
|
+
s.exec[name] = state;
|
|
42
|
+
if (busy && !was) s.workingSince[name] = now();
|
|
43
|
+
if (!busy) { delete s.workingSince[name]; delete s.stopping[name]; }
|
|
44
|
+
if (known && name === s.current && busy !== was) deps.announce(tf(busy ? "chat.a11yWorking" : "chat.a11yIdle", name));
|
|
45
|
+
}
|
|
46
|
+
function applyStatus(d) {
|
|
47
|
+
if (!d || !Array.isArray(d.instances)) return;
|
|
48
|
+
for (const i of d.instances) {
|
|
49
|
+
noteExec(i.name, (i.execution_state !== undefined ? i.execution_state : i.state) ?? null);
|
|
50
|
+
const waits = i.state === "awaiting_input" ? (typeof i.interaction_summary === "string" ? i.interaction_summary : "") : null;
|
|
51
|
+
if (i.name === s.current && waits != null && s.awaiting[i.name] == null) deps.announce(tf("chat.awaiting", i.name));
|
|
52
|
+
s.awaiting[i.name] = waits;
|
|
53
|
+
}
|
|
54
|
+
changed(null);
|
|
55
|
+
}
|
|
56
|
+
function applyActivity(d) {
|
|
57
|
+
if (!d || typeof d.instance !== "string") return;
|
|
58
|
+
noteExec(d.instance, d.state ?? null);
|
|
59
|
+
changed(d.instance);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Messages: one way in, from the stream, a poll or the history; one entry per boot+id.
|
|
63
|
+
function ingest(m) {
|
|
64
|
+
if (!m || typeof m.instance !== "string") return;
|
|
65
|
+
const before = s.msgs[m.instance];
|
|
66
|
+
s.msgs[m.instance] = R().mergeMessages(before, [m], 500);
|
|
67
|
+
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));
|
|
68
|
+
if (s.msgs[m.instance] !== before) changed(m.instance, "msgs");
|
|
69
|
+
}
|
|
70
|
+
function applyDeliveries(list) {
|
|
71
|
+
if (!Array.isArray(list)) return;
|
|
72
|
+
const touched = new Set();
|
|
73
|
+
for (const d of list) {
|
|
74
|
+
if (!d || typeof d.instance !== "string") continue;
|
|
75
|
+
const before = s.msgs[d.instance];
|
|
76
|
+
s.msgs[d.instance] = R().applyDelivery(before, d.messageId, d.delivery);
|
|
77
|
+
if (s.msgs[d.instance] !== before) touched.add(d.instance);
|
|
78
|
+
}
|
|
79
|
+
for (const i of touched) changed(i, "msgs");
|
|
80
|
+
}
|
|
81
|
+
/** Read an instance's history once per page (the first time a person opens its chat). */
|
|
82
|
+
async function openHistory(name, lease) {
|
|
83
|
+
if (s.historyRead.has(name)) return false;
|
|
84
|
+
s.historyRead.add(name);
|
|
85
|
+
try {
|
|
86
|
+
const r = await api("GET", `/ui/history?instance=${encodeURIComponent(name)}&limit=200`);
|
|
87
|
+
if (!r || !Array.isArray(r.messages)) { s.historyRead.delete(name); return true; }
|
|
88
|
+
// Kept even when the person has moved on: the store is page-lived, and the answer belongs to `name`.
|
|
89
|
+
s.msgs[name] = R().mergeMessages(s.msgs[name], r.messages, 500);
|
|
90
|
+
changed(name, "msgs");
|
|
91
|
+
} catch { s.historyRead.delete(name); /* the live stream still works; the next visit tries again */ }
|
|
92
|
+
void lease;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Prompts the fleet has open (hang / clean exit / interactive input): the same buttons the platform shows.
|
|
97
|
+
function onPrompt(p) {
|
|
98
|
+
if (!p || typeof p.nonce !== "string") return;
|
|
99
|
+
const had = s.prompts[p.nonce];
|
|
100
|
+
const fresh = !had;
|
|
101
|
+
// A prompt already held (being answered, or claimed from Needs you) keeps its busy/answered state: a repeat of it
|
|
102
|
+
// only refreshes its text and buttons (#1463 review).
|
|
103
|
+
if (!had) s.prompts[p.nonce] = { ...p, resolved: false, busy: false };
|
|
104
|
+
else if (!had.resolved) Object.assign(had, { text: p.text, actions: p.actions, expiresAt: p.expiresAt });
|
|
105
|
+
if (p.instance === s.current) changed(p.instance);
|
|
106
|
+
else if (fresh) deps.toast(tf("chat.promptFor", p.instance, p.text), false);
|
|
107
|
+
}
|
|
108
|
+
/** The open prompts, on every stream (re)connect and with every poll: a prompt answered or expired while this page
|
|
109
|
+
* was not listening is caught up here, and no timer ever re-reads anything. */
|
|
110
|
+
function applyPrompts(list) {
|
|
111
|
+
if (!Array.isArray(list)) return;
|
|
112
|
+
const open = new Set(list.map(p => p.nonce));
|
|
113
|
+
for (const p of list) {
|
|
114
|
+
const had = s.prompts[p.nonce];
|
|
115
|
+
if (!had) s.prompts[p.nonce] = { ...p, resolved: false, busy: false };
|
|
116
|
+
else if (!had.resolved) Object.assign(had, { text: p.text, actions: p.actions, expiresAt: p.expiresAt });
|
|
117
|
+
}
|
|
118
|
+
for (const n of Object.keys(s.prompts)) if (!s.prompts[n].resolved && !open.has(n)) s.prompts[n].resolved = true;
|
|
119
|
+
changed(null);
|
|
120
|
+
}
|
|
121
|
+
function resolvePrompt(d) {
|
|
122
|
+
const p = d && s.prompts[d.nonce];
|
|
123
|
+
if (!p) return;
|
|
124
|
+
p.resolved = true; p.busy = false;
|
|
125
|
+
if (d.outcome) p.outcome = d.outcome;
|
|
126
|
+
deps.setTimeout(() => { if (s.prompts[d.nonce] === p) { delete s.prompts[d.nonce]; changed(p.instance); } }, 60000);
|
|
127
|
+
changed(p.instance);
|
|
128
|
+
}
|
|
129
|
+
async function answerPrompt(p, action) {
|
|
130
|
+
if (p.busy || p.resolved) return;
|
|
131
|
+
p.busy = true; changed(p.instance);
|
|
132
|
+
let r;
|
|
133
|
+
try { r = await api("POST", "/ui/prompt", { instance: p.instance, nonce: p.nonce, action }); }
|
|
134
|
+
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
135
|
+
if (r && r.answered) return; // the outcome arrives as prompt_resolved
|
|
136
|
+
const latest = s.prompts[p.nonce];
|
|
137
|
+
for (const q of latest && latest !== p ? [p, latest] : [p]) { q.busy = false; if (r && r.gone) q.resolved = true; }
|
|
138
|
+
changed(p.instance);
|
|
139
|
+
deps.toast(r && r.error ? r.error : t("chat.disconnected"), false);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Answer a prompt known only by its nonce (Needs you, #1386): the page's one claim on it. A prompt this store does not
|
|
143
|
+
* hold yet is entered now, busy, so the chat, a second click and a later hydration all see the same claim; the next
|
|
144
|
+
* prompts list or prompt_resolved settles it, as for any prompt.
|
|
145
|
+
*/
|
|
146
|
+
function answerByNonce(item, action) {
|
|
147
|
+
if (!item || typeof item.nonce !== "string") return Promise.resolve();
|
|
148
|
+
let p = s.prompts[item.nonce];
|
|
149
|
+
if (!p) p = s.prompts[item.nonce] = { instance: item.instance, nonce: item.nonce, text: item.text || item.detail || "", actions: item.actions || [], resolved: false, busy: false };
|
|
150
|
+
return answerPrompt(p, action);
|
|
151
|
+
}
|
|
152
|
+
/** A claim taken elsewhere before this store existed (Needs you, before Chat booted): held here from now on. */
|
|
153
|
+
function holdPrompt(item) {
|
|
154
|
+
if (!item || typeof item.nonce !== "string") return;
|
|
155
|
+
let p = s.prompts[item.nonce];
|
|
156
|
+
if (!p) p = s.prompts[item.nonce] = { instance: item.instance, nonce: item.nonce, text: item.text || item.detail || "", actions: item.actions || [], resolved: false, busy: false };
|
|
157
|
+
if (!p.resolved) { p.busy = true; changed(p.instance); }
|
|
158
|
+
}
|
|
159
|
+
/** That claim's answer was refused: the prompt can be answered again (unless it was settled meanwhile). */
|
|
160
|
+
function releasePrompt(nonce) {
|
|
161
|
+
const p = s.prompts[nonce];
|
|
162
|
+
if (p && !p.resolved) { p.busy = false; changed(p.instance); }
|
|
163
|
+
}
|
|
164
|
+
const promptsFor = (instance) => Object.values(s.prompts).filter(p => p.instance === instance);
|
|
165
|
+
|
|
166
|
+
// ── Sending. Never lose anything: a failed send's files go back to that chat's list and its text back into its own
|
|
167
|
+
// composer when that is empty, otherwise it is kept for its chat and offered back (Put back).
|
|
168
|
+
async function uploadFile(target, file) {
|
|
169
|
+
const r = await deps.fetch(`/ui/upload?instance=${encodeURIComponent(target)}`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": file.type || "application/octet-stream", "X-Agend-Filename": encodeURIComponent(file.name || "file") },
|
|
172
|
+
body: file,
|
|
173
|
+
});
|
|
174
|
+
let body = {};
|
|
175
|
+
try { body = await r.json(); } catch { /* not JSON */ }
|
|
176
|
+
if (!r.ok || !body || typeof body.id !== "string") throw new Error(body && body.error ? `${file.name}: ${body.error}` : `${file.name}: upload failed (${r.status})`);
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
async function send(target) {
|
|
180
|
+
const txt = (s.drafts[target] || "").trim();
|
|
181
|
+
const files = (s.pendingFiles[target] || []).slice();
|
|
182
|
+
if ((!txt && !files.length) || !target || s.sending[target]) return;
|
|
183
|
+
s.sending[target] = true;
|
|
184
|
+
s.drafts[target] = "";
|
|
185
|
+
s.pendingFiles[target] = []; s.inFlightFiles[target] = files;
|
|
186
|
+
changed(target);
|
|
187
|
+
changed(target, "sent"); // your own message: the thread goes to the bottom
|
|
188
|
+
let r;
|
|
189
|
+
try {
|
|
190
|
+
const ids = [];
|
|
191
|
+
for (const f of files) ids.push((await uploadFile(target, f)).id);
|
|
192
|
+
r = await api("POST", "/ui/send", ids.length ? { instance: target, message: txt, attachments: ids } : { instance: target, message: txt });
|
|
193
|
+
} catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
194
|
+
s.sending[target] = false;
|
|
195
|
+
delete s.inFlightFiles[target];
|
|
196
|
+
if (r && !r.error) { changed(target); return; }
|
|
197
|
+
if (files.length) s.pendingFiles[target] = R().checkFiles(files, s.pendingFiles[target] || []).kept;
|
|
198
|
+
if (txt) {
|
|
199
|
+
const shown = s.current === target;
|
|
200
|
+
if (R().settleFailedSend(target, s.current, shown, shown ? (s.drafts[target] || "") : "") === "restore") s.drafts[target] = txt;
|
|
201
|
+
else s.failedSends[target] = s.failedSends[target] ? `${s.failedSends[target]}\n${txt}` : txt;
|
|
202
|
+
}
|
|
203
|
+
changed(target);
|
|
204
|
+
deps.toast(r && r.error ? r.error : t("chat.disconnected"), false);
|
|
205
|
+
}
|
|
206
|
+
function addFiles(target, list) {
|
|
207
|
+
if (!target) return;
|
|
208
|
+
const inFlight = s.inFlightFiles[target] || [];
|
|
209
|
+
const res = R().checkFiles(inFlight.concat(s.pendingFiles[target] || []), [...(list || [])]);
|
|
210
|
+
s.pendingFiles[target] = res.kept.slice(inFlight.length);
|
|
211
|
+
for (const x of res.rejected) deps.toast(`${x.name}: ${t(`chat.file_${x.reason.replace(/-/g, "_")}`)}`, false);
|
|
212
|
+
changed(target);
|
|
213
|
+
}
|
|
214
|
+
function removeFile(target, index) { (s.pendingFiles[target] || []).splice(index, 1); changed(target); }
|
|
215
|
+
/** A long paste as a .txt file when it fits beside what is attached; true when it was attached. */
|
|
216
|
+
function attachPastedText(target, text) {
|
|
217
|
+
if (!target) return false;
|
|
218
|
+
const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
|
219
|
+
const f = new File([text], `pasted-${stamp}.txt`, { type: "text/plain" });
|
|
220
|
+
const taken = (s.inFlightFiles[target] || []).concat(s.pendingFiles[target] || []);
|
|
221
|
+
if (R().checkFiles(taken, [f]).rejected.length) { deps.toast(t("chat.pasteKeptAsText")); return false; }
|
|
222
|
+
pastedTexts.set(f, text);
|
|
223
|
+
addFiles(target, [f]);
|
|
224
|
+
deps.toast(tf("chat.pasteAttached", text.length.toLocaleString()));
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
/** A pasted file back into the composer as text. */
|
|
228
|
+
function fileBackAsText(target, index) {
|
|
229
|
+
const f = (s.pendingFiles[target] || [])[index];
|
|
230
|
+
if (!f || !pastedTexts.has(f)) return;
|
|
231
|
+
s.pendingFiles[target].splice(index, 1);
|
|
232
|
+
const draft = s.drafts[target] || "";
|
|
233
|
+
s.drafts[target] = draft ? `${draft}\n${pastedTexts.get(f)}` : pastedTexts.get(f);
|
|
234
|
+
changed(target);
|
|
235
|
+
}
|
|
236
|
+
const isPasted = (f) => pastedTexts.has(f);
|
|
237
|
+
function putBack(target) {
|
|
238
|
+
const kept = s.failedSends[target];
|
|
239
|
+
if (!kept) return;
|
|
240
|
+
s.drafts[target] = R().putBack(kept, s.drafts[target] || "");
|
|
241
|
+
delete s.failedSends[target];
|
|
242
|
+
changed(target);
|
|
243
|
+
}
|
|
244
|
+
function discardFailed(target) { delete s.failedSends[target]; changed(target); }
|
|
245
|
+
/** Typing: kept for its chat, no redraw (the composer already shows it). */
|
|
246
|
+
function setDraft(target, value) { s.drafts[target] = value; }
|
|
247
|
+
|
|
248
|
+
// ── Stop the reply under way (the composer's Stop, or Esc). Set before the request goes, so a held Esc or a second
|
|
249
|
+
// click sends nothing more (#1317); "Stopping…" until the agent goes idle, or a minute passes.
|
|
250
|
+
async function cancelReply(target) {
|
|
251
|
+
if (!target || s.cancelling[target]) return;
|
|
252
|
+
s.cancelling[target] = true;
|
|
253
|
+
changed(target);
|
|
254
|
+
let r;
|
|
255
|
+
try { r = await api("POST", `/ui/cancel/${encodeURIComponent(target)}`); }
|
|
256
|
+
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
257
|
+
delete s.cancelling[target];
|
|
258
|
+
if (r && !r.error) {
|
|
259
|
+
if (R().isBusy(s.exec[target])) {
|
|
260
|
+
s.stopping[target] = true;
|
|
261
|
+
deps.setTimeout(() => { if (s.stopping[target]) { delete s.stopping[target]; changed(target); } }, 60000);
|
|
262
|
+
}
|
|
263
|
+
deps.toast(`${target}: ${t("chat.stopSent")}`);
|
|
264
|
+
} else deps.toast(r && r.error ? r.error : t("chat.disconnected"), false);
|
|
265
|
+
changed(target);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Wire the store to the app's stream, for the life of the page. */
|
|
269
|
+
function attach(stream) {
|
|
270
|
+
stream.on("status", applyStatus);
|
|
271
|
+
stream.on("activity", applyActivity);
|
|
272
|
+
stream.on("message", ingest);
|
|
273
|
+
stream.on("delivery", (d) => applyDeliveries([d]));
|
|
274
|
+
stream.on("deliveries", applyDeliveries);
|
|
275
|
+
stream.on("prompt", onPrompt);
|
|
276
|
+
stream.on("prompts", applyPrompts);
|
|
277
|
+
stream.on("prompt_resolved", resolvePrompt);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
state: s,
|
|
282
|
+
subscribe(fn) { subs.add(fn); return () => subs.delete(fn); },
|
|
283
|
+
setCurrent(name) { s.current = name; },
|
|
284
|
+
attach, applyStatus, applyActivity, ingest, applyDeliveries, openHistory,
|
|
285
|
+
onPrompt, applyPrompts, resolvePrompt, answerPrompt, answerByNonce, holdPrompt, releasePrompt, promptsFor,
|
|
286
|
+
uploadFile, send, addFiles, removeFile, attachPastedText, fileBackAsText, isPasted, putBack, discardFailed, setDraft,
|
|
287
|
+
cancelReply, isUser,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The chat's words (the chat.* namespace of the app's one dictionary, #1408 §4), in English and zh-TW.
|
|
2
|
+
import { register } from "/assets/app-i18n.js";
|
|
3
|
+
|
|
4
|
+
register("chat", {
|
|
5
|
+
en: {
|
|
6
|
+
tourTitle: "Welcome to web chat", tourStep: "{0} of {1}", tourBack: "Back", tourNext: "Next", tourDone: "Got it", tourSkip: "Skip",
|
|
7
|
+
tour1: "Pick an instance in the list to chat with it. On a phone, ☰ opens the list.",
|
|
8
|
+
tour2: "It is the same conversation as its Telegram or Discord chat: what you send here shows up there too, and the agent's replies show up in both.",
|
|
9
|
+
tour3: "Attach files and images with 📎, or paste or drop them on the chat.",
|
|
10
|
+
tour4: "While the agent works, Stop reply appears in the message box: it interrupts the current reply. The instance itself keeps running.",
|
|
11
|
+
tour5: "A “needs you” badge means the agent is waiting on its terminal, for example a permission question. Answer it from the buttons in its chat.",
|
|
12
|
+
a11yWorking: "{0} started working", a11yIdle: "{0} finished", a11yReplied: "{0} replied", conversationWith: "Conversation with {0}",
|
|
13
|
+
stopping: "Stopping {0}…", awaiting: "{0} is waiting for your input", approxNote: "Read from the terminal, so approximate", elapsedTitle: "Time since this page saw it start (approximate)",
|
|
14
|
+
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",
|
|
15
|
+
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line · Esc stops the reply", attachTitle: "Attach files (images, PDF, text — or paste / drop them)",
|
|
16
|
+
pvPreview: "Preview", pvStop: "Stop", pvDownload: "Download", pvMenu: "Preview options", pvHtml: "HTML",
|
|
17
|
+
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews on this device — they may be able to send data out", pvDisallow: "Stop allowing HTML previews on this device",
|
|
18
|
+
pvConfirm: "Allow HTML previews on this device?\n\nA 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.",
|
|
19
|
+
pvNever: "Never preview HTML on this device", pvNeverUndo: "Let this device preview HTML again", pvStarting: "Starting the preview…",
|
|
20
|
+
pvChanged: "Preview stopped — the message changed. Preview again.", pvTruncated: "Truncated — ask the agent to send it as a .html file.",
|
|
21
|
+
previews: "Allow HTML previews on this device",
|
|
22
|
+
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",
|
|
23
|
+
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",
|
|
24
|
+
promptAnswering: "Answering…", promptGone: "This prompt is no longer open", promptFor: "{0}: {1}",
|
|
25
|
+
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",
|
|
26
|
+
file_empty: "empty file", file_too_large: "over 10 MB", file_too_many: "at most 5 files per message", file_too_much: "the files together would be over 25 MB", removeFile: "Remove", notSent: "Not sent:", putBack: "Put back", discard: "Discard",
|
|
27
|
+
noMessages: "No messages yet", sendFirst: "Send a message to start the conversation",
|
|
28
|
+
pick: "Pick an instance to start", pickHint: "Choose one in the list, or create a new one.", notFound: "No instance called “{0}”", notFoundHint: "It may have been deleted or renamed.", backToList: "Open the instance list",
|
|
29
|
+
details: "Instance details", start: "Start instance", stopInstance: "Stop instance", restart: "Restart instance", delete: "Delete instance…",
|
|
30
|
+
started: "{0} started", stoppedInst: "{0} stopped", restarted: "{0} restarted", instanceDeleted: "{0} was deleted",
|
|
31
|
+
deleteTitle: "Delete {0}?", deleteHelp: "This removes the instance from the fleet and stops it. Its working directory is kept. Type “delete {0}” to confirm.", deleteGo: "Delete", cancel: "Cancel",
|
|
32
|
+
loadFailed: "Failed to load", tour: "Tour", tourBtnTitle: "Show the web chat tour again",
|
|
33
|
+
dName: "Name", dStatus: "Status", dDisplay: "Display name", dDescription: "Description", dDirectory: "Directory", dBackend: "Backend", dModel: "Model", dEffort: "Effort",
|
|
34
|
+
dCost: "Cost", dCostNote: "(session accumulated)", dContext: "Context", dRate5: "Rate 5h", dRate7: "Rate 7d", dActivity: "Recent activity", dInstance: "Instance", dRuntime: "Runtime",
|
|
35
|
+
configured: "(configured)", fleetDefault: "(fleet default)",
|
|
36
|
+
},
|
|
37
|
+
"zh-TW": {
|
|
38
|
+
tourTitle: "歡迎使用網頁聊天", tourStep: "{0} / {1}", tourBack: "上一步", tourNext: "下一步", tourDone: "知道了", tourSkip: "略過",
|
|
39
|
+
tour1: "在清單裡選一個 instance,就能跟它對話。手機上請按 ☰ 打開清單。",
|
|
40
|
+
tour2: "這和它在 Telegram 或 Discord 上的是同一段對話:在這裡送出的訊息那邊也看得到,Agent 的回覆兩邊都會出現。",
|
|
41
|
+
tour3: "按 📎 附上檔案和圖片,也可以直接貼上或拖到聊天裡。",
|
|
42
|
+
tour4: "Agent 工作時,輸入框會出現「中止回覆」:中斷目前的回覆,instance 本身會繼續運行。",
|
|
43
|
+
tour5: "看到「等你回應」標記,表示 Agent 在終端機上等你,例如權限詢問。到它的聊天裡按按鈕回應即可。",
|
|
44
|
+
a11yWorking: "{0} 開始工作", a11yIdle: "{0} 已完成", a11yReplied: "{0} 已回覆", conversationWith: "與 {0} 的對話",
|
|
45
|
+
stopping: "正在中止 {0}…", awaiting: "{0} 正在等你回應", approxNote: "從終端畫面判讀,僅供參考", elapsedTitle: "從這個頁面看到它開始工作起算(約略)",
|
|
46
|
+
dropHere: "放開即可附加檔案", asText: "改為文字", pasteAttached: "貼上的長文字({0} 個字元)已改為文字檔附加", pasteKeptAsText: "附件已滿,貼上的長文字保留為一般文字",
|
|
47
|
+
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行 · Esc 中止回覆", attachTitle: "附加檔案(圖片、PDF、文字檔,也可以貼上或拖曳)",
|
|
48
|
+
pvPreview: "預覽", pvStop: "停止", pvDownload: "下載", pvMenu: "預覽選項", pvHtml: "HTML",
|
|
49
|
+
pvAllowButton: "允許預覽", pvAllow: "允許這台裝置預覽 HTML(預覽可能把資料傳出去)", pvDisallow: "不再允許這台裝置預覽 HTML",
|
|
50
|
+
pvConfirm: "要允許這台裝置預覽 HTML 嗎?\n\n預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
|
51
|
+
pvNever: "這台裝置永不預覽 HTML", pvNeverUndo: "讓這台裝置再次可以預覽 HTML", pvStarting: "正在啟動預覽…",
|
|
52
|
+
pvChanged: "預覽已停止——訊息內容變了。請再預覽一次。", pvTruncated: "內容被截斷——請 agent 改用 .html 檔案傳送。",
|
|
53
|
+
previews: "允許這台裝置預覽 HTML",
|
|
54
|
+
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
55
|
+
disconnected: "已斷線", tick_queued: "排在前一則訊息之後", tick_processing: "已交給 agent", tick_delivered: "agent 已收到", tick_failed: "未送達", tick_cancelled: "已被中止取消",
|
|
56
|
+
promptAnswering: "回覆中…", promptGone: "這個提示已經結束", promptFor: "{0}:{1}",
|
|
57
|
+
working: "{0} 處理中…", stuck: "{0} 似乎卡住了", stopReply: "中止回覆", stopReplyTitle: "中止目前的回覆(同 Esc),並取消還在排隊的訊息。instance 會繼續運行。", stopSent: "已中止",
|
|
58
|
+
file_empty: "空檔案", file_too_large: "超過 10 MB", file_too_many: "每則訊息最多 5 個檔案", file_too_much: "檔案合計會超過 25 MB", removeFile: "移除", notSent: "未送出:", putBack: "放回輸入框", discard: "捨棄",
|
|
59
|
+
noMessages: "尚無訊息", sendFirst: "傳送訊息以開始對話",
|
|
60
|
+
pick: "選一個 instance 開始", pickHint: "在清單裡選一個,或建立新的。", notFound: "沒有叫「{0}」的 instance", notFoundHint: "它可能已被刪除或改名。", backToList: "打開 instance 清單",
|
|
61
|
+
details: "Instance 詳細資料", start: "啟動 instance", stopInstance: "停止 instance", restart: "重新啟動 instance", delete: "刪除 instance…",
|
|
62
|
+
started: "{0} 已啟動", stoppedInst: "{0} 已停止", restarted: "{0} 已重新啟動", instanceDeleted: "{0} 已刪除",
|
|
63
|
+
deleteTitle: "要刪除 {0} 嗎?", deleteHelp: "這會把 instance 從 fleet 移除並停止它,工作目錄會保留。請輸入「delete {0}」確認。", deleteGo: "刪除", cancel: "取消",
|
|
64
|
+
loadFailed: "載入失敗", tour: "導覽", tourBtnTitle: "再看一次網頁聊天導覽",
|
|
65
|
+
dName: "名稱", dStatus: "狀態", dDisplay: "顯示名稱", dDescription: "說明", dDirectory: "目錄", dBackend: "Backend", dModel: "模型", dEffort: "Effort",
|
|
66
|
+
dCost: "花費", dCostNote: "(本次 session 累計)", dContext: "Context", dRate5: "5 小時額度", dRate7: "7 天額度", dActivity: "最近活動", dInstance: "Instance", dRuntime: "執行環境",
|
|
67
|
+
configured: "(設定值)", fleetDefault: "(fleet 預設)",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// #1408 §0/§4: the conversation thread — the keyed DOM renderer #1307 and #1306 reviewed, now owned by the chat panel
|
|
2
|
+
// through a ref. Preact never manages these children: a node here may hold a live preview iframe, and an iframe that
|
|
3
|
+
// is moved or re-parented reloads. So:
|
|
4
|
+
// - a new message is appended; a changed one (its ticks) is replaced; the rest are left alone — an opened code block
|
|
5
|
+
// stays open, a selection survives, where the reader scrolled stays theirs;
|
|
6
|
+
// - before ONE node is replaced, moved or removed: AgendPreview.stopIn(that node) — frames elsewhere keep running;
|
|
7
|
+
// - when the whole thread goes (the chat left, another instance opened): AgendPreview.stopAll("leave").
|
|
8
|
+
// Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
|
|
9
|
+
import "./chat-render.js";
|
|
10
|
+
import "./preview.js";
|
|
11
|
+
|
|
12
|
+
const R = () => globalThis.AgendChatRender;
|
|
13
|
+
const P = () => globalThis.AgendPreview;
|
|
14
|
+
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>';
|
|
15
|
+
|
|
16
|
+
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
17
|
+
export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); }
|
|
18
|
+
export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
22
|
+
* setPreviewOptIn(on), copyText(text) → Promise<boolean>, download(code) }.
|
|
23
|
+
*/
|
|
24
|
+
export function createThread(list, scroller, opts) {
|
|
25
|
+
const nodes = new Map(); // key → { html, node }
|
|
26
|
+
let msgs = [];
|
|
27
|
+
let unseen = 0;
|
|
28
|
+
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
29
|
+
const tr = opts.t, trf = opts.tf;
|
|
30
|
+
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
31
|
+
|
|
32
|
+
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") });
|
|
33
|
+
|
|
34
|
+
function msgHtml(x) {
|
|
35
|
+
const u = opts.isUser(x);
|
|
36
|
+
const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
37
|
+
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
38
|
+
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
39
|
+
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>`;
|
|
40
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span><span class="time">${time}</span>${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, x.role === "agent" ? { htmlCards: true } : undefined)}</div>${R().attachmentsHtml(x.attachments)}</div>${tools}</div>`;
|
|
41
|
+
}
|
|
42
|
+
function msgNode(html, x, stoppedKeys) {
|
|
43
|
+
const tpl = list.ownerDocument.createElement("template");
|
|
44
|
+
tpl.innerHTML = html;
|
|
45
|
+
const node = tpl.content.firstElementChild;
|
|
46
|
+
decorateCode(node);
|
|
47
|
+
if (x) decorateHtmlCards(node, x, stoppedKeys || []);
|
|
48
|
+
return node;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Code blocks: a header (language, Wrap, Copy); a long one is folded until opened. Built from nodes.
|
|
52
|
+
function decorateCode(root) {
|
|
53
|
+
const doc = list.ownerDocument;
|
|
54
|
+
for (const pre of root.querySelectorAll(".md pre")) {
|
|
55
|
+
const box = doc.createElement("div"); box.className = "codeblock";
|
|
56
|
+
const head = doc.createElement("div"); head.className = "cb-head";
|
|
57
|
+
const lang = doc.createElement("span"); lang.className = "cb-lang"; lang.textContent = pre.dataset.lang || "text";
|
|
58
|
+
const wrap = doc.createElement("button"); wrap.type = "button"; wrap.className = "chip-btn"; wrap.dataset.act = "toggleWrap"; wrap.textContent = tr("chat.wrap");
|
|
59
|
+
const copy = doc.createElement("button"); copy.type = "button"; copy.className = "chip-btn"; copy.dataset.act = "copyCode"; copy.textContent = tr("chat.copy");
|
|
60
|
+
head.append(lang, wrap, copy);
|
|
61
|
+
pre.replaceWith(box); box.append(head, pre);
|
|
62
|
+
const lines = R().lineCount(pre.textContent);
|
|
63
|
+
if (lines > R().CODE_FOLD_LINES) {
|
|
64
|
+
box.classList.add("folded");
|
|
65
|
+
const more = doc.createElement("button"); more.type = "button"; more.className = "cb-more"; more.dataset.act = "toggleFold";
|
|
66
|
+
more.dataset.lines = String(lines); more.textContent = trf("chat.showAll", lines);
|
|
67
|
+
box.append(more);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// HTML cards (#1306): each ```html fence of a server-marked agent message gets a card under its code block. Building
|
|
73
|
+
// a card makes no frame; Preview does (AgendPreview.start → mountPreview).
|
|
74
|
+
function decorateHtmlCards(node, x, stoppedKeys) {
|
|
75
|
+
const doc = list.ownerDocument;
|
|
76
|
+
const fences = R().htmlFences(x.text);
|
|
77
|
+
for (const ph of node.querySelectorAll(".html-card[data-card]")) {
|
|
78
|
+
const n = Number(ph.dataset.card), fence = fences[n];
|
|
79
|
+
if (!fence) continue;
|
|
80
|
+
const key = `${msgKey(x)}:f${n}`;
|
|
81
|
+
ph.textContent = "";
|
|
82
|
+
if (ph.classList.contains("truncated") || !fence.terminated) {
|
|
83
|
+
const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
87
|
+
const head = el("div", "pv-head");
|
|
88
|
+
const label = el("span", "pv-label", tr("chat.pvHtml"));
|
|
89
|
+
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
90
|
+
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
91
|
+
const dl = el("button", "btn btn-sm btn-ghost pv-dl", tr("chat.pvDownload")); dl.type = "button";
|
|
92
|
+
const menu = el("details", "pv-menu");
|
|
93
|
+
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
94
|
+
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
95
|
+
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
96
|
+
menu.append(sum, optB, neverB);
|
|
97
|
+
head.append(label, run, stopB, dl, menu);
|
|
98
|
+
const note = el("div", "pv-note");
|
|
99
|
+
const banner = el("div", "pv-banner", P().BANNER); banner.hidden = true;
|
|
100
|
+
const holder = el("div", "pv-holder");
|
|
101
|
+
ph.append(head, note, banner, holder);
|
|
102
|
+
let state = "idle";
|
|
103
|
+
const refresh = (reason) => {
|
|
104
|
+
const a = P().availability();
|
|
105
|
+
const going = state === "starting" || state === "running";
|
|
106
|
+
run.hidden = !a.ok || going;
|
|
107
|
+
stopB.hidden = !going;
|
|
108
|
+
banner.hidden = !going;
|
|
109
|
+
note.textContent = reason != null ? reason : going ? (state === "starting" ? tr("chat.pvStarting") : "") : a.ok ? "" : a.reason;
|
|
110
|
+
optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
|
|
111
|
+
neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
|
|
112
|
+
};
|
|
113
|
+
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
114
|
+
run.onclick = () => P().start(key, holder, fence.code, ui);
|
|
115
|
+
stopB.onclick = () => P().stop(key, "stopped", "");
|
|
116
|
+
dl.onclick = () => opts.download(fence.code);
|
|
117
|
+
optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
|
|
118
|
+
neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
|
|
119
|
+
cardRefresh.set(ph, refresh);
|
|
120
|
+
refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Re-read this device's preview choice on every card in this thread. */
|
|
124
|
+
function refreshCards() {
|
|
125
|
+
for (const node of list.querySelectorAll(".html-card[data-card]")) { const f = cardRefresh.get(node); if (f) f(); }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const nearBottom = () => R().isNearBottom(scroller.scrollTop, scroller.clientHeight, scroller.scrollHeight);
|
|
129
|
+
function reportJump() { opts.onJump({ show: !nearBottom(), unseen }); }
|
|
130
|
+
|
|
131
|
+
/** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
|
|
132
|
+
function render(next, renderOpts = {}) {
|
|
133
|
+
const stick = nearBottom();
|
|
134
|
+
msgs = next || [];
|
|
135
|
+
if (!msgs.length) {
|
|
136
|
+
for (const v of nodes.values()) stopIn(v.node);
|
|
137
|
+
nodes.clear();
|
|
138
|
+
list.textContent = "";
|
|
139
|
+
opts.onEmpty(true);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
opts.onEmpty(false);
|
|
143
|
+
const keep = new Set(msgs.map(msgKey));
|
|
144
|
+
for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
|
|
145
|
+
let prev = null, added = 0;
|
|
146
|
+
for (const x of msgs) {
|
|
147
|
+
const key = msgKey(x), html = msgHtml(x);
|
|
148
|
+
let have = nodes.get(key);
|
|
149
|
+
if (!have || have.html !== html) {
|
|
150
|
+
const stopped = have ? stopIn(have.node) : [];
|
|
151
|
+
const node = msgNode(html, x, stopped);
|
|
152
|
+
if (have) have.node.replaceWith(node); else added++;
|
|
153
|
+
have = { html, node }; nodes.set(key, have);
|
|
154
|
+
}
|
|
155
|
+
const want = prev ? prev.nextSibling : list.firstChild;
|
|
156
|
+
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
157
|
+
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
158
|
+
prev = have.node;
|
|
159
|
+
}
|
|
160
|
+
if ("restore" in renderOpts) {
|
|
161
|
+
unseen = 0;
|
|
162
|
+
scroller.scrollTop = renderOpts.restore == null ? scroller.scrollHeight : renderOpts.restore;
|
|
163
|
+
} else if (stick) scroller.scrollTop = scroller.scrollHeight;
|
|
164
|
+
else unseen += added;
|
|
165
|
+
reportJump();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function onScroll() {
|
|
169
|
+
const bottom = nearBottom();
|
|
170
|
+
if (bottom) unseen = 0;
|
|
171
|
+
reportJump();
|
|
172
|
+
return bottom ? null : scroller.scrollTop;
|
|
173
|
+
}
|
|
174
|
+
function jumpLatest() { scroller.scrollTop = scroller.scrollHeight; unseen = 0; reportJump(); }
|
|
175
|
+
|
|
176
|
+
async function copyFrom(button, text) {
|
|
177
|
+
const ok = await opts.copyText(text);
|
|
178
|
+
if (button.classList.contains("icon-only")) {
|
|
179
|
+
button.classList.add(ok ? "done" : "fail");
|
|
180
|
+
button.title = ok ? tr("chat.copied") : tr("chat.copyFailed"); button.setAttribute("aria-label", button.title);
|
|
181
|
+
setTimeout(() => { button.classList.remove("done", "fail"); button.title = tr("chat.copyMessage"); button.setAttribute("aria-label", button.title); }, 1200);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const was = button.dataset.label || button.textContent; button.dataset.label = was;
|
|
185
|
+
button.textContent = ok ? tr("chat.copied") : tr("chat.copyFailed");
|
|
186
|
+
setTimeout(() => { button.textContent = was; }, 1200);
|
|
187
|
+
}
|
|
188
|
+
/** The thread's own buttons (Copy, Wrap, fold), delegated from the thread element. */
|
|
189
|
+
function onClick(e) {
|
|
190
|
+
const el = e.target && e.target.closest ? e.target.closest("[data-act]") : null;
|
|
191
|
+
if (!el || !list.contains(el)) return;
|
|
192
|
+
const act = el.dataset.act;
|
|
193
|
+
if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
|
|
194
|
+
else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
|
|
195
|
+
else if (act === "toggleFold") {
|
|
196
|
+
const box = el.closest(".codeblock"); if (!box) return;
|
|
197
|
+
const folded = box.classList.toggle("folded");
|
|
198
|
+
el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.showLess");
|
|
199
|
+
} else if (act === "toggleWrap") opts.toggleWrap();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
203
|
+
function dispose() {
|
|
204
|
+
if (P()) P().stopAll("leave");
|
|
205
|
+
nodes.clear();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { render, onScroll, onClick, jumpLatest, refreshCards, dispose, nodes, get unseen() { return unseen; } };
|
|
209
|
+
}
|