@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +599 -127
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +82 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +45 -83
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +8 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +13 -5
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +38 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +50 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
package/dist/ui/chat-thread.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
|
|
9
9
|
import "./chat-render.js";
|
|
10
10
|
import "./preview.js";
|
|
11
|
+
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
11
12
|
|
|
12
13
|
const R = () => globalThis.AgendChatRender;
|
|
13
14
|
const P = () => globalThis.AgendPreview;
|
|
@@ -44,7 +45,23 @@ export function createThread(list, scroller, opts) {
|
|
|
44
45
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
45
46
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
46
47
|
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>`;
|
|
47
|
-
|
|
48
|
+
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
49
|
+
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, x.role === "agent" ? { htmlCards: true } : undefined)}${buttons}</div>${tools}</div>`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
53
|
+
* nothing else. Ended (chosen / expired) or a click in flight: every button off; the chosen one marked, and who.
|
|
54
|
+
*/
|
|
55
|
+
function replyButtonsHtml(b) {
|
|
56
|
+
if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
|
|
57
|
+
const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
|
|
58
|
+
const off = b.state !== "open" || busy;
|
|
59
|
+
const items = b.labels.map((label, i) => {
|
|
60
|
+
const chosen = b.state === "chosen" && b.chosen === i;
|
|
61
|
+
return `<button type="button" class="btn btn-sm rb-btn${chosen ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}"${off ? " disabled" : ""}>${escAttr(chosen ? `✓ ${label}` : label)}</button>`;
|
|
62
|
+
}).join("");
|
|
63
|
+
const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
|
|
64
|
+
return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
48
65
|
}
|
|
49
66
|
function msgNode(html, x, stoppedKeys) {
|
|
50
67
|
const tpl = list.ownerDocument.createElement("template");
|
|
@@ -76,72 +93,108 @@ export function createThread(list, scroller, opts) {
|
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
// HTML cards (#1306): each ```html fence of a server-marked agent message gets a card under its code block
|
|
80
|
-
//
|
|
96
|
+
// HTML cards (#1306): each ```html fence of a server-marked agent message gets a card under its code block, and each
|
|
97
|
+
// .html/.htm file it attached gets one under its files (§6.1, Q4). Building a card makes no frame and fetches
|
|
98
|
+
// nothing; Preview does (AgendPreview.start → mountPreview), after reading an attachment's HTML on that click.
|
|
81
99
|
function decorateHtmlCards(node, x, stoppedKeys) {
|
|
82
100
|
const doc = list.ownerDocument;
|
|
83
101
|
const fences = R().htmlFences(x.text);
|
|
84
102
|
for (const ph of node.querySelectorAll(".html-card[data-card]")) {
|
|
85
103
|
const n = Number(ph.dataset.card), fence = fences[n];
|
|
86
104
|
if (!fence) continue;
|
|
87
|
-
const key = `${msgKey(x)}:f${n}`;
|
|
88
105
|
ph.textContent = "";
|
|
89
106
|
if (ph.classList.contains("truncated") || !fence.terminated) {
|
|
90
107
|
const note = doc.createElement("div"); note.className = "pv-note"; note.textContent = tr("chat.pvTruncated"); ph.append(note);
|
|
91
108
|
continue;
|
|
92
109
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (toPanel) toPanel.type = "button";
|
|
102
|
-
const menu = el("details", "pv-menu");
|
|
103
|
-
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
104
|
-
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
105
|
-
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
106
|
-
menu.append(sum, optB, neverB);
|
|
107
|
-
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), dl, menu);
|
|
108
|
-
const note = el("div", "pv-note");
|
|
109
|
-
const banner = el("div", "pv-banner", P().BANNER); banner.hidden = true;
|
|
110
|
-
const holder = el("div", "pv-holder");
|
|
111
|
-
ph.append(head, note, banner, holder);
|
|
112
|
-
let state = "idle";
|
|
113
|
-
const refresh = (reason) => {
|
|
114
|
-
const a = P().availability();
|
|
115
|
-
const going = state === "starting" || state === "running";
|
|
116
|
-
const inPanel = !!panel && panel.shown() === key; // #1481: the panel shows this block; the card points there
|
|
117
|
-
run.hidden = !a.ok || going || inPanel;
|
|
118
|
-
if (toPanel) toPanel.hidden = inPanel;
|
|
119
|
-
stopB.hidden = !going;
|
|
120
|
-
banner.hidden = !going;
|
|
121
|
-
note.textContent = reason != null ? reason : going ? (state === "starting" ? tr("chat.pvStarting") : "") : inPanel ? tr("chat.pvInPanel") : a.ok ? "" : a.reason;
|
|
122
|
-
optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
|
|
123
|
-
neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
|
|
124
|
-
};
|
|
125
|
-
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
126
|
-
run.onclick = () => P().start(key, holder, fence.code, ui);
|
|
127
|
-
stopB.onclick = () => P().stop(key, "stopped", "");
|
|
128
|
-
// A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
|
|
129
|
-
// panel with its own Preview to click.
|
|
130
|
-
if (toPanel) toPanel.onclick = () => {
|
|
131
|
-
const moving = P().running(key);
|
|
132
|
-
if (moving) P().stop(key, "stopped", "");
|
|
133
|
-
panel.open({ key, instance: x.instance, msgKey: msgKey(x), n, code: fence.code, sender: x.sender, ts: x.ts }, { run: moving });
|
|
134
|
-
};
|
|
135
|
-
dl.onclick = () => opts.download(fence.code);
|
|
136
|
-
optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
|
|
137
|
-
neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
|
|
138
|
-
cardRefresh.set(ph, refresh);
|
|
139
|
-
refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
|
|
110
|
+
buildCard(ph, x, `${msgKey(x)}:f${n}`, { n, code: fence.code }, stoppedKeys);
|
|
111
|
+
}
|
|
112
|
+
const atts = Array.isArray(x.attachments) ? x.attachments : [];
|
|
113
|
+
for (const ph of node.querySelectorAll(".html-card[data-att]")) {
|
|
114
|
+
const att = atts.find(a => a && a.id === ph.dataset.att);
|
|
115
|
+
if (!att || !R().isHtmlAttachment(att)) continue;
|
|
116
|
+
ph.textContent = "";
|
|
117
|
+
buildCard(ph, x, `${msgKey(x)}:a${att.id}`, { att: { id: att.id, name: att.name, size: att.size } }, stoppedKeys);
|
|
140
118
|
}
|
|
141
119
|
}
|
|
120
|
+
/** Why an attachment's HTML could not be read, for the card's note. */
|
|
121
|
+
const attReason = (reason) => tr(reason === "over" ? "chat.pvAttOver" : reason === "gone" ? "chat.pvAttGone" : "chat.pvAttFailed");
|
|
122
|
+
/** One card: `source` is { n, code } for a fence or { att } for an attachment (its HTML read on the first click). */
|
|
123
|
+
function buildCard(ph, x, key, source, stoppedKeys) {
|
|
124
|
+
const doc = list.ownerDocument;
|
|
125
|
+
const el = (tag, cls, text) => { const e = doc.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
126
|
+
const att = source.att || null;
|
|
127
|
+
let code = att ? null : source.code;
|
|
128
|
+
const head = el("div", "pv-head");
|
|
129
|
+
const label = el("span", "pv-label", att ? `${tr("chat.pvHtml")} · ${att.name}` : tr("chat.pvHtml"));
|
|
130
|
+
const run = el("button", "btn btn-sm pv-run", tr("chat.pvPreview")); run.type = "button";
|
|
131
|
+
const stopB = el("button", "btn btn-sm pv-stop", tr("chat.pvStop")); stopB.type = "button"; stopB.hidden = true;
|
|
132
|
+
// An attachment already has its download link (the file itself) right above the card.
|
|
133
|
+
const dl = att ? null : el("button", "btn btn-sm btn-ghost pv-dl", tr("chat.pvDownload"));
|
|
134
|
+
if (dl) dl.type = "button";
|
|
135
|
+
const panel = opts.panel || null;
|
|
136
|
+
const toPanel = panel ? el("button", "btn btn-sm btn-ghost pv-open", tr("chat.pvOpenPanel")) : null;
|
|
137
|
+
if (toPanel) toPanel.type = "button";
|
|
138
|
+
const menu = el("details", "pv-menu");
|
|
139
|
+
const sum = el("summary", null, "⋯"); sum.title = tr("chat.pvMenu"); sum.setAttribute("aria-label", tr("chat.pvMenu"));
|
|
140
|
+
const optB = el("button", "chip-btn pv-opt"); optB.type = "button";
|
|
141
|
+
const neverB = el("button", "chip-btn pv-never"); neverB.type = "button";
|
|
142
|
+
menu.append(sum, optB, neverB);
|
|
143
|
+
head.append(label, run, stopB, ...(toPanel ? [toPanel] : []), ...(dl ? [dl] : []), menu);
|
|
144
|
+
const note = el("div", "pv-note");
|
|
145
|
+
const banner = el("div", "pv-banner", P().BANNER); banner.hidden = true;
|
|
146
|
+
const holder = el("div", "pv-holder");
|
|
147
|
+
ph.append(head, note, banner, holder);
|
|
148
|
+
let state = "idle";
|
|
149
|
+
// Reading an attachment: one read per card at a time (a second click while it reads does nothing), and a read
|
|
150
|
+
// whose card was stopped, replaced or left meanwhile is dropped (`reading` moved on, or the card left the page).
|
|
151
|
+
let reading = 0, reads = 0;
|
|
152
|
+
const refresh = (reason) => {
|
|
153
|
+
const a = P().availability();
|
|
154
|
+
const going = state === "starting" || state === "running";
|
|
155
|
+
const inPanel = !!panel && panel.shown() === key; // #1481: the panel shows this block; the card points there
|
|
156
|
+
run.hidden = !a.ok || going || inPanel || !!reading;
|
|
157
|
+
if (toPanel) toPanel.hidden = inPanel || !!reading;
|
|
158
|
+
stopB.hidden = !going && !reading;
|
|
159
|
+
banner.hidden = !going;
|
|
160
|
+
note.textContent = reason != null ? reason : reading ? tr("chat.pvLoading") : going ? (state === "starting" ? tr("chat.pvStarting") : "") : inPanel ? tr("chat.pvInPanel") : a.ok ? "" : a.reason;
|
|
161
|
+
optB.textContent = P().optedIn() ? tr("chat.pvDisallow") : tr("chat.pvAllow");
|
|
162
|
+
neverB.textContent = P().never() ? tr("chat.pvNeverUndo") : tr("chat.pvNever");
|
|
163
|
+
};
|
|
164
|
+
/** The HTML, then `then(code)`: at once for a fence or an attachment already read; otherwise read it first. */
|
|
165
|
+
const withCode = async (then) => {
|
|
166
|
+
if (code != null) { then(code); return; }
|
|
167
|
+
if (reading) return;
|
|
168
|
+
const mine = reading = ++reads;
|
|
169
|
+
refresh();
|
|
170
|
+
const r = await loadHtmlAttachment(att);
|
|
171
|
+
if (reading !== mine || !ph.isConnected) return;
|
|
172
|
+
reading = 0;
|
|
173
|
+
if (!r.ok) { refresh(attReason(r.reason)); return; }
|
|
174
|
+
code = r.code;
|
|
175
|
+
refresh();
|
|
176
|
+
then(code);
|
|
177
|
+
};
|
|
178
|
+
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
179
|
+
// Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
|
|
180
|
+
run.onclick = () => { if (P().availability().ok) withCode((c) => P().start(key, holder, c, ui)); };
|
|
181
|
+
stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
|
|
182
|
+
// A running preview moves: it stops here and starts in the panel (the click that ran it). An idle one opens the
|
|
183
|
+
// panel with its own Preview to click.
|
|
184
|
+
if (toPanel) toPanel.onclick = () => {
|
|
185
|
+
const moving = P().running(key);
|
|
186
|
+
if (moving) P().stop(key, "stopped", "");
|
|
187
|
+
withCode((c) => panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: moving }));
|
|
188
|
+
};
|
|
189
|
+
if (dl) dl.onclick = () => opts.download(code);
|
|
190
|
+
optB.onclick = () => { menu.open = false; opts.setPreviewOptIn(!P().optedIn()); };
|
|
191
|
+
neverB.onclick = () => { menu.open = false; P().setNever(!P().never()); refreshCards(); };
|
|
192
|
+
cardRefresh.set(ph, refresh);
|
|
193
|
+
refresh(stoppedKeys.includes(key) ? tr("chat.pvChanged") : null);
|
|
194
|
+
}
|
|
142
195
|
/** Re-read this device's preview choice on every card in this thread. */
|
|
143
196
|
function refreshCards() {
|
|
144
|
-
for (const node of list.querySelectorAll(".html-card[data-card]")) { const f = cardRefresh.get(node); if (f) f(); }
|
|
197
|
+
for (const node of list.querySelectorAll(".html-card[data-card], .html-card[data-att]")) { const f = cardRefresh.get(node); if (f) f(); }
|
|
145
198
|
}
|
|
146
199
|
|
|
147
200
|
const nearBottom = () => R().isNearBottom(scroller.scrollTop, scroller.clientHeight, scroller.scrollHeight);
|
|
@@ -233,6 +286,10 @@ export function createThread(list, scroller, opts) {
|
|
|
233
286
|
const folded = box.classList.toggle("folded");
|
|
234
287
|
el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.showLess");
|
|
235
288
|
} else if (act === "toggleWrap") opts.toggleWrap();
|
|
289
|
+
else if (act === "replyButton") {
|
|
290
|
+
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
291
|
+
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
292
|
+
}
|
|
236
293
|
}
|
|
237
294
|
|
|
238
295
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// #1519 P7 (docs/design/ux-onboarding-walkthrough.md §5.1): the first-run card. A fleet with no chat connection (a
|
|
2
|
+
// web-only fleet) opens Chat and Settings with it: "Connect a chat app" opens the setup wizard (in Settings, where its
|
|
3
|
+
// progress is shown), "Use the web only" hides it in this browser. It stays until a connection exists or it is dismissed.
|
|
4
|
+
import { html } from "/assets/app-html.js";
|
|
5
|
+
import { register, t } from "/assets/app-i18n.js";
|
|
6
|
+
import { appStore, createStore, useStore } from "/assets/app-store.js";
|
|
7
|
+
import { navigate } from "/assets/app-nav.js";
|
|
8
|
+
import { settingsPath } from "/assets/app-route.js";
|
|
9
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
10
|
+
|
|
11
|
+
register("firstRun", {
|
|
12
|
+
en: {
|
|
13
|
+
title: "Connect a chat app",
|
|
14
|
+
body: "AgEnD works in this browser on its own. To talk to your agents from Telegram or Discord too, add a bot: the setup wizard checks its token and connects it to an agent.",
|
|
15
|
+
connect: "Connect a chat app", webOnly: "Use the web only",
|
|
16
|
+
},
|
|
17
|
+
"zh-TW": {
|
|
18
|
+
title: "連接聊天應用程式",
|
|
19
|
+
body: "AgEnD 只用這個瀏覽器也能運作。想同時從 Telegram 或 Discord 跟你的 agent 對話,就加一個機器人:設定精靈會檢查它的 token,並把它接到一個 agent。",
|
|
20
|
+
connect: "連接聊天應用程式", webOnly: "只用網頁",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const DISMISS_KEY = "agend_firstrun_dismissed";
|
|
25
|
+
function readDismissed() { try { return localStorage.getItem(DISMISS_KEY) === "1"; } catch { return false; } }
|
|
26
|
+
// `dismissed`: this page's own answer (it holds even when the browser keeps no storage); a stored one is read each time.
|
|
27
|
+
const local = createStore({ dismissed: false, wizard: false });
|
|
28
|
+
|
|
29
|
+
export function dismissFirstRun() {
|
|
30
|
+
try { localStorage.setItem(DISMISS_KEY, "1"); } catch { /* this page only */ }
|
|
31
|
+
local.set({ dismissed: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** For the tests: a fresh page's state. */
|
|
35
|
+
export function resetFirstRun() { local.set({ dismissed: false, wizard: false }); }
|
|
36
|
+
|
|
37
|
+
/** Settings asks once, when it has its data: was it opened by "Connect a chat app" (open the wizard)? */
|
|
38
|
+
export function takeWizardRequest() {
|
|
39
|
+
if (!local.get().wizard) return false;
|
|
40
|
+
local.set({ wizard: false });
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Shown on a known web-only fleet (no connection) until dismissed. */
|
|
45
|
+
export function useFirstRun() {
|
|
46
|
+
const app = useStore(appStore);
|
|
47
|
+
const { dismissed } = useStore(local);
|
|
48
|
+
return app.ready && app.connections === 0 && !dismissed && !readDismissed();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** onConnect: Settings opens its wizard in place; elsewhere the card goes to Settings, which opens it. */
|
|
52
|
+
export function FirstRunCard({ onConnect }) {
|
|
53
|
+
const shown = useFirstRun();
|
|
54
|
+
if (!shown) return null;
|
|
55
|
+
const connect = onConnect || (() => { local.set({ wizard: true }); navigate(settingsPath("bots")); });
|
|
56
|
+
return html`<section class="card first-run" aria-labelledby="first-run-title">
|
|
57
|
+
<h3 id="first-run-title"><${Icon} name="plug" size=${14} /> ${t("firstRun.title")}</h3>
|
|
58
|
+
<p>${t("firstRun.body")}</p>
|
|
59
|
+
<div class="dlg-inline-actions">
|
|
60
|
+
<button type="button" class="btn btn-primary" onClick=${connect}>${t("firstRun.connect")}</button>
|
|
61
|
+
<button type="button" class="btn btn-ghost" onClick=${dismissFirstRun}>${t("firstRun.webOnly")}</button>
|
|
62
|
+
</div>
|
|
63
|
+
</section>`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// #1306 §6.1 (Q4): an agent's .html / .htm attachment gets the same preview card as a ```html block. Its HTML is read
|
|
2
|
+
// from /ui/file/<id> — same origin, this session's cookie — only on a click (Preview, Open in panel), never to show a
|
|
3
|
+
// card. What runs is then handed to the preview frame exactly like a block's (AgendPreview.start): the file URL itself
|
|
4
|
+
// is never opened or framed on the dashboard's origin.
|
|
5
|
+
|
|
6
|
+
/** The same cap as a block's (preview.js LIMITS.maxBytes): UTF-8 bytes of the HTML. */
|
|
7
|
+
export const HTML_ATTACHMENT_MAX = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read an attachment's HTML: { ok: true, code } or { ok: false, reason } with reason "over" (the file is over the
|
|
11
|
+
* cap: its listed size is checked before anything is fetched, the bytes again after), "gone" (the fleet no longer
|
|
12
|
+
* serves that id — the file changed, or the fleet restarted), or "failed".
|
|
13
|
+
*/
|
|
14
|
+
export async function loadHtmlAttachment(att, fetchImpl = globalThis.fetch) {
|
|
15
|
+
if (!att || typeof att.id !== "string" || !/^[0-9a-f]{32}$/.test(att.id)) return { ok: false, reason: "failed" };
|
|
16
|
+
if (typeof att.size === "number" && att.size > HTML_ATTACHMENT_MAX) return { ok: false, reason: "over" };
|
|
17
|
+
let res;
|
|
18
|
+
try { res = await fetchImpl(`/ui/file/${att.id}`, { credentials: "same-origin", cache: "no-store" }); }
|
|
19
|
+
catch { return { ok: false, reason: "failed" }; }
|
|
20
|
+
if (!res || !res.ok) return { ok: false, reason: res && res.status === 404 ? "gone" : "failed" };
|
|
21
|
+
let bytes;
|
|
22
|
+
try { bytes = await res.arrayBuffer(); } catch { return { ok: false, reason: "failed" }; }
|
|
23
|
+
if (bytes.byteLength > HTML_ATTACHMENT_MAX) return { ok: false, reason: "over" };
|
|
24
|
+
// UTF-8, as a block's text is; a BOM is dropped.
|
|
25
|
+
return { ok: true, code: new TextDecoder("utf-8").decode(bytes) };
|
|
26
|
+
}
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -10,7 +10,7 @@ import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-
|
|
|
10
10
|
import { t } from "/assets/app-i18n.js";
|
|
11
11
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
12
12
|
import { useLease } from "/assets/app-ctx.js";
|
|
13
|
-
import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
|
|
13
|
+
import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
|
|
14
14
|
import { navigate } from "/assets/app-nav.js";
|
|
15
15
|
import { Menu } from "/assets/ui-menu.js";
|
|
16
16
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
@@ -25,6 +25,7 @@ import { createThread, msgKey } from "./chat-thread.js";
|
|
|
25
25
|
import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.js";
|
|
26
26
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
27
27
|
import { installTour, refreshTourSpot, startTour } from "./chat-tour.js";
|
|
28
|
+
import { FirstRunCard } from "./first-run.js";
|
|
28
29
|
|
|
29
30
|
const R = () => globalThis.AgendChatRender;
|
|
30
31
|
const P = () => globalThis.AgendPreview;
|
|
@@ -99,9 +100,9 @@ async function copyText(text) {
|
|
|
99
100
|
return ok;
|
|
100
101
|
}
|
|
101
102
|
/** The HTML as a file: a blob of the source as application/octet-stream, saved as reply.html — never opened. */
|
|
102
|
-
function downloadHtml(code) {
|
|
103
|
+
function downloadHtml(code, name) {
|
|
103
104
|
const url = URL.createObjectURL(new Blob([code], { type: "application/octet-stream" }));
|
|
104
|
-
const a = document.createElement("a"); a.href = url; a.download = "reply.html"; a.className = "offscreen";
|
|
105
|
+
const a = document.createElement("a"); a.href = url; a.download = typeof name === "string" && name.trim() ? name.trim() : "reply.html"; a.className = "offscreen";
|
|
105
106
|
document.body.appendChild(a); a.click(); a.remove();
|
|
106
107
|
URL.revokeObjectURL(url);
|
|
107
108
|
}
|
|
@@ -127,9 +128,14 @@ export function ChatPanel({ route, navKey }) {
|
|
|
127
128
|
}, [name, !!inst]);
|
|
128
129
|
useEffect(() => { setTitle(name || t("app.chat")); }, [name, navKey]);
|
|
129
130
|
if (!name) {
|
|
131
|
+
// #1519 P7: a web-only fleet starts here with the first-run card; with no agent at all, the way to make one.
|
|
132
|
+
const none = app.ready && !app.instances.length;
|
|
130
133
|
return html`<div class="panel p-chat"><${PanelHeader} title=${t("app.chat")} />
|
|
131
|
-
<div class="panel-body center"
|
|
132
|
-
|
|
134
|
+
<div class="panel-body center"><div class="first-run-stack"><${FirstRunCard} />
|
|
135
|
+
${none ? html`<${Empty} icon="bot" title=${t("app.noInstances")} hint=${t("app.noInstancesHint")}
|
|
136
|
+
action=${html`<button type="button" class="btn btn-primary" onClick=${requestNewInstance}><${Icon} name="plus" size=${16} />${t("app.newInstance")}</button>`} />`
|
|
137
|
+
: html`<${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
|
|
138
|
+
action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} />`}</div></div></div>`;
|
|
133
139
|
}
|
|
134
140
|
if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
|
|
135
141
|
if (!inst) return html`<${NotFound} name=${name} />`;
|
|
@@ -221,6 +227,8 @@ function Thread({ name, th }) {
|
|
|
221
227
|
const thread = createThread(list.current, scroller.current, {
|
|
222
228
|
t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
|
|
223
229
|
setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
|
|
230
|
+
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
231
|
+
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
224
232
|
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
225
233
|
});
|
|
226
234
|
th.current = thread;
|
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -36,8 +36,8 @@ register("fleet", {
|
|
|
36
36
|
channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
|
|
37
37
|
allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
|
|
38
38
|
saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
|
|
39
|
-
newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "
|
|
40
|
-
topicRequired: "
|
|
39
|
+
newInstance: "New instance", directory: "Directory", directoryHint: "Optional — a workspace is created if empty", topic: "Name", topicHint: "From the directory name",
|
|
40
|
+
topicRequired: "Name is required when Directory is empty", description: "Description", descriptionHint: "What this instance does", model: "Model", modelHint: "e.g. sonnet, opus, gpt-5",
|
|
41
41
|
branch: "Branch (git worktree)", branchHint: "e.g. feature-x", tags: "Tags", tagsHint: "comma-separated, e.g. dev, review", systemPrompt: "System prompt", systemPromptHint: "Custom instructions for this instance",
|
|
42
42
|
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
43
43
|
},
|
|
@@ -56,8 +56,8 @@ register("fleet", {
|
|
|
56
56
|
channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
|
|
57
57
|
allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
|
|
58
58
|
saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
|
|
59
|
-
newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "
|
|
60
|
-
topicRequired: "
|
|
59
|
+
newInstance: "新增 instance", directory: "目錄", directoryHint: "可留空——留空會自動建立工作區", topic: "名稱", topicHint: "預設取自目錄名稱",
|
|
60
|
+
topicRequired: "目錄留空時必須填寫名稱", description: "說明", descriptionHint: "這個 instance 做什麼", model: "模型", modelHint: "例如 sonnet、opus、gpt-5",
|
|
61
61
|
branch: "分支(git worktree)", branchHint: "例如 feature-x", tags: "標籤", tagsHint: "以逗號分隔,例如 dev, review", systemPrompt: "System prompt", systemPromptHint: "這個 instance 的自訂指示",
|
|
62
62
|
fleetDefault: "使用 fleet 預設", notInstalled: "(未安裝)", deprecated: "(已淘汰)", instanceCreated: "Instance 已建立",
|
|
63
63
|
},
|
|
@@ -359,7 +359,7 @@ function CreateTeam({ onClose, onDone }) {
|
|
|
359
359
|
</${FormDialog}>`;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
/** "New instance": the sidebar's
|
|
362
|
+
/** "New instance": the sidebar's + opens it from anywhere (#1408 step 3 merges it with Settings' create flow). */
|
|
363
363
|
export function CreateInstanceDialog({ onClose }) {
|
|
364
364
|
const lease = useLease("create-instance");
|
|
365
365
|
const [backends, setBackends] = useState(null);
|
|
@@ -25,7 +25,7 @@ import { api, attach, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
|
25
25
|
import { dismissOperation, operationActive, restartFleet, startOperation, takeLeftover } from "./settings-apply.js";
|
|
26
26
|
import {
|
|
27
27
|
ACCESS_MODES, AGENT_MODES, DEFAULT_SCHEMA, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS, VISIBILITY_MODES, batchImpact,
|
|
28
|
-
changedFields, channelId, channelsOf, chLabel, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
|
|
28
|
+
changedFields, channelId, channelsOf, chLabel, connectionState, effectiveSummary, fleetModel, fromYaml, fullModelRequests, groupAgents,
|
|
29
29
|
hasOwn, impactOf, localeOptions, defaultsImpact, requestImpact, nonNegative, positive, replyGuardSupported, shortName, STATUS_ORDER, toYaml,
|
|
30
30
|
visibilityDefault, worstImpact,
|
|
31
31
|
} from "./settings-model.js";
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
} from "./settings-dialogs.js";
|
|
36
36
|
import { SetupWizard } from "./settings-wizard.js";
|
|
37
37
|
import { CreateInstanceDialog } from "./panel-fleet.js";
|
|
38
|
+
import { FirstRunCard, takeWizardRequest } from "./first-run.js";
|
|
38
39
|
|
|
39
40
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
40
41
|
const ICONS = { agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
|
|
@@ -197,6 +198,8 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
197
198
|
};
|
|
198
199
|
|
|
199
200
|
const ctx = data && !data.error ? makeCtx(data, setData, stage, unstage, reload, reloadLive, () => mount.current()) : null;
|
|
201
|
+
// #1519 P7: "Connect a chat app" on the first-run card elsewhere came here for the wizard: it opens once the data is in.
|
|
202
|
+
useEffect(() => { if (ctx && takeWizardRequest()) setDialog({ kind: "wizard", key: navKey }); }, [!!ctx, navKey]);
|
|
200
203
|
const sectionBody = !data ? html`<${Skeleton} lines=${6} />`
|
|
201
204
|
: data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
|
|
202
205
|
: section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
@@ -220,6 +223,7 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
220
223
|
<input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
|
|
221
224
|
${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
|
|
222
225
|
<${PendingChanges} inline=${true} />
|
|
226
|
+
${ctx && (section === "agents" || section === "bots") ? html`<${FirstRunCard} onConnect=${() => setDialog({ kind: "wizard", key: navKey })} />` : null}
|
|
223
227
|
${sectionBody}
|
|
224
228
|
</div></div>
|
|
225
229
|
${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} />` : null}
|
|
@@ -378,7 +382,12 @@ function Bots({ ctx, search, openDialog }) {
|
|
|
378
382
|
return html`${head}
|
|
379
383
|
${!rows.length ? html`<${Empty} icon="plug" title=${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")} />` : html`<div class="s-list">${rows.map(({ ch, i }) => {
|
|
380
384
|
const users = (ch.access && ch.access.allowed_users) || [];
|
|
381
|
-
const
|
|
385
|
+
const meta = ctx.connections.find((c, j) => channelId(c, j) === channelId(ch, i)) || {};
|
|
386
|
+
const token = meta.token_present;
|
|
387
|
+
// #1519 P6: this connection's own state (not the fleet's "Connected") — a stopped one says so; a known problem says
|
|
388
|
+
// what to do (P3: Message Content intent; a rejected token).
|
|
389
|
+
const state = connectionState(meta, ctx.fleetUp);
|
|
390
|
+
const stateText = tn(state.key, state.bot);
|
|
382
391
|
// #1408 step 5: one line on a desktop — who it is, its facts in one clipped line (all of them in its tooltip),
|
|
383
392
|
// then the cluster; on a phone the facts go under it. It no longer wraps fact by fact.
|
|
384
393
|
const facts = [ch.bot_token_env || tn("noTokenEnv"),
|
|
@@ -386,13 +395,13 @@ function Bots({ ctx, search, openDialog }) {
|
|
|
386
395
|
tn("accessTag", (ch.access && ch.access.mode) || "locked"),
|
|
387
396
|
users.length ? `${users.slice(0, 3).join(", ")}${users.length > 3 ? ` +${users.length - 3}` : ""}` : tn("noAllowedUsers")].filter(Boolean).join(" · ");
|
|
388
397
|
return html`<div key=${channelId(ch, i)} class="s-row s-conn">
|
|
389
|
-
<span class="s-ident"><span class=${`dot ${
|
|
398
|
+
<span class="s-ident"><span class=${`dot ${state.cls}`} title=${stateText} aria-hidden="true"></span>
|
|
390
399
|
<span class="s-name">${ch.type === "telegram" ? "Telegram" : "Discord"}</span>
|
|
391
400
|
<span class=${`tag${i === 0 ? "" : " persona"}`}>${ch.id || chLabel(i)}</span></span>
|
|
392
401
|
<span class="s-facts" title=${facts}>${facts}</span>
|
|
393
402
|
<span class="s-actions">
|
|
394
403
|
<span class=${`tag${token ? "" : " warn"}`}>${token ? tn("tokenConfigured") : tn("tokenMissing")}</span>
|
|
395
|
-
<span class=${`s-state ${
|
|
404
|
+
<span class=${`s-state ${state.cls}`} title=${stateText}>${stateText}</span>
|
|
396
405
|
<button type="button" class="btn btn-sm" onClick=${() => openDialog({ kind: "bot", id: channelId(ch, i) })}>${tn("settingsButton")}</button></span>
|
|
397
406
|
</div>`;
|
|
398
407
|
})}</div>`}
|
|
@@ -671,7 +680,7 @@ function Developer({ ctx }) {
|
|
|
671
680
|
${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div>
|
|
672
681
|
<button type="button" class="btn btn-sm" onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
|
|
673
682
|
<button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
|
|
674
|
-
<button type="button" class="btn btn-sm" title
|
|
683
|
+
<button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
|
|
675
684
|
</div>
|
|
676
685
|
${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
677
686
|
<div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
|
|
@@ -699,7 +708,8 @@ function OperationCard({ op, schema }) {
|
|
|
699
708
|
const phaseBusy = ["writing", "posting", "watching"].includes(op.phase);
|
|
700
709
|
const job = op.job;
|
|
701
710
|
const rows = (job && job.targets) || [];
|
|
702
|
-
|
|
711
|
+
// A fleet row of kind "hot" is connections starting without a restart (#1519 P6), not AgEnD restarting.
|
|
712
|
+
const fleetRestarting = job && job.status === "running" && rows.some((r) => r.target === "fleet" && r.status === "running" && r.kind !== "hot");
|
|
703
713
|
const needsRestart = rows.some((r) => r.status === "restart-required");
|
|
704
714
|
const failedRows = rows.some((r) => r.status === "failed");
|
|
705
715
|
const mismatch = schema && schema.fleet_signature_mismatch;
|
|
@@ -732,11 +742,11 @@ function OperationCard({ op, schema }) {
|
|
|
732
742
|
${op.steps.length ? html`<ul class="s-op-steps">${op.steps.map((s, i) => html`<li key=${i} class=${`s-step ${s.status}`}>
|
|
733
743
|
<span class="s-step-icon" aria-hidden="true">${icon(s.status) ? html`<${Icon} name=${icon(s.status)} size=${14} />` : "·"}</span>
|
|
734
744
|
<span class="grow">${s.label}</span>
|
|
735
|
-
<span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : impactText(s.impact)}</span></li>`)}</ul>` : null}
|
|
745
|
+
<span class="note">${s.status === "waiting" ? tn("stepWaiting") : s.status === "skipped" ? tn("stepSkipped") : s.status === "failed" ? (s.error || tn("applyFailed")) : s.note || impactText(s.impact)}</span></li>`)}</ul>` : null}
|
|
736
746
|
${rows.length ? html`<ul class="s-op-steps">${rows.map((r) => html`<li key=${r.target} class=${`s-step ${r.status}`}>
|
|
737
747
|
<span class="s-step-icon" aria-hidden="true">${r.status === "done" ? html`<${Icon} name="check" size=${14} />` : r.status === "failed" ? html`<${Icon} name="alert" size=${14} />` : r.status === "restart-required" ? html`<${Icon} name="restart" size=${14} />` : "·"}</span>
|
|
738
748
|
<span class="grow">${r.target === "fleet" ? "AgEnD" : shortName(r.target, {})}</span>
|
|
739
|
-
<span class="note">${r.target === "fleet" && r.status === "running" ? tn("restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
|
|
749
|
+
<span class="note">${r.target === "fleet" && r.status === "running" ? tn(r.kind === "hot" ? "connStartingNow" : "restartFleetTitle") : r.status === "restart-required" ? tn("applyRestartNeeded")
|
|
740
750
|
: r.settled_by === "no-change" ? tn("applyNoChange") : r.settled_by === "fleet-restart" ? tn("viaRestart") : r.kind === "hot" ? tn("impactNow") : tn("impactAgent")}</span>
|
|
741
751
|
${r.error ? html`<span class="feedback error">${r.error}</span>` : null}</li>`)}</ul>` : null}
|
|
742
752
|
${job && job.error ? html`<p class="feedback error">${job.error}</p>` : null}
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -12,13 +12,14 @@ import { t } from "/assets/app-i18n.js";
|
|
|
12
12
|
import { Icon } from "/assets/ui-icons.js";
|
|
13
13
|
import "./chat-render.js";
|
|
14
14
|
import "./preview.js";
|
|
15
|
+
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
15
16
|
|
|
16
17
|
const R = () => globalThis.AgendChatRender;
|
|
17
18
|
const P = () => globalThis.AgendPreview;
|
|
18
19
|
|
|
19
20
|
// ── The panel's state, one per page (the chat shows one instance at a time) ──
|
|
20
21
|
|
|
21
|
-
/** open: { key, instance, msgKey, n, code, sender, ts } | null; run: start it on mount (a moved run). */
|
|
22
|
+
/** open: { key, instance, msgKey, n | att, code, sender, ts } | null (`att`: an attached .html file, #1306 Q4); run: start it on mount (a moved run). */
|
|
22
23
|
let state = { open: null, run: 0 };
|
|
23
24
|
const subs = new Set();
|
|
24
25
|
function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
@@ -54,6 +55,22 @@ export function newerVersion(open, msgs, keyOf) {
|
|
|
54
55
|
if (!open || !Array.isArray(msgs)) return null;
|
|
55
56
|
const at = msgs.findIndex(x => keyOf(x) === open.msgKey);
|
|
56
57
|
if (at < 0) return null;
|
|
58
|
+
// An attached file: a later agent reply attaching a file of the same name is its newer version. Its HTML is read
|
|
59
|
+
// when the person picks it (code: null here), as on a card.
|
|
60
|
+
if (open.att) {
|
|
61
|
+
const name = String(open.att.name || "").trim().toLowerCase();
|
|
62
|
+
for (let i = msgs.length - 1; i > at; i--) {
|
|
63
|
+
const x = msgs[i];
|
|
64
|
+
if (x.role !== "agent" || !Array.isArray(x.attachments)) continue;
|
|
65
|
+
for (let k = x.attachments.length - 1; k >= 0; k--) {
|
|
66
|
+
const a = x.attachments[k];
|
|
67
|
+
if (!R().isHtmlAttachment(a) || a.name.trim().toLowerCase() !== name) continue;
|
|
68
|
+
const msgKey = keyOf(x);
|
|
69
|
+
return { key: `${msgKey}:a${a.id}`, instance: open.instance, msgKey, att: { id: a.id, name: a.name, size: a.size }, code: null, sender: x.sender, ts: x.ts };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
57
74
|
const title = htmlTitle(open.code);
|
|
58
75
|
for (let i = msgs.length - 1; i > at; i--) {
|
|
59
76
|
const x = msgs[i];
|
|
@@ -93,6 +110,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
93
110
|
const [view, setView] = useState("preview");
|
|
94
111
|
const [run, setRun] = useState({ state: "idle", reason: "" });
|
|
95
112
|
const [newer, setNewer] = useState(null);
|
|
113
|
+
const [newerNote, setNewerNote] = useState("");
|
|
114
|
+
const reading = useRef(null); // the newer version whose file is being read (one at a time)
|
|
96
115
|
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
97
116
|
const open = st.open && st.open.instance === name ? st.open : null;
|
|
98
117
|
|
|
@@ -120,6 +139,7 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
120
139
|
}, [open && open.key, st.run]);
|
|
121
140
|
// A later reply with a newer version of this block: offered, not swapped in.
|
|
122
141
|
useEffect(() => {
|
|
142
|
+
setNewerNote(""); reading.current = null;
|
|
123
143
|
if (!open) { setNewer(null); return undefined; }
|
|
124
144
|
const check = () => setNewer(newerVersion(open, msgs() || [], keyOf));
|
|
125
145
|
check();
|
|
@@ -162,6 +182,21 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
162
182
|
el.addEventListener("pointerup", up);
|
|
163
183
|
el.addEventListener("pointercancel", up);
|
|
164
184
|
};
|
|
185
|
+
// Show the newer version: an attached file's HTML is read first. A read the panel moved on from is dropped.
|
|
186
|
+
const showNewer = async () => {
|
|
187
|
+
if (!newer || reading.current) return;
|
|
188
|
+
if (newer.code != null) { openPanel(newer); return; }
|
|
189
|
+
const was = open && open.key, mine = reading.current = newer;
|
|
190
|
+
setNewerNote(t("chat.pvLoading"));
|
|
191
|
+
const r = await loadHtmlAttachment(newer.att);
|
|
192
|
+
if (reading.current !== mine) return;
|
|
193
|
+
reading.current = null;
|
|
194
|
+
const now = panelStore.get().open;
|
|
195
|
+
if (!now || now.key !== was) return;
|
|
196
|
+
if (!r.ok) { setNewerNote(t(r.reason === "over" ? "chat.pvAttOver" : r.reason === "gone" ? "chat.pvAttGone" : "chat.pvAttFailed")); return; }
|
|
197
|
+
openPanel({ ...newer, code: r.code });
|
|
198
|
+
};
|
|
199
|
+
const fileName = open && open.att ? open.att.name : undefined;
|
|
165
200
|
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
166
201
|
const onPanelKey = (e) => { if (e.key === "Escape" && !e.isComposing) { e.preventDefault(); closePanel(); } };
|
|
167
202
|
|
|
@@ -188,10 +223,10 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
188
223
|
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
189
224
|
<button type="button" class="btn btn-sm pv-panel-stop" title=${t("chat.pvStop")} onClick=${stop}><${Icon} name="stop" size=${14} /><span class="lbl">${t("chat.pvStop")}</span></button>`
|
|
190
225
|
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-run" title=${t("chat.pvPreview")} disabled=${!a.ok} onClick=${start}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvPreview")}</span></button>`}
|
|
191
|
-
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
226
|
+
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code, fileName)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
192
227
|
</div>
|
|
193
228
|
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
194
|
-
<button type="button" class="btn btn-sm pv-newer-show" onClick=${
|
|
229
|
+
<button type="button" class="btn btn-sm pv-newer-show" onClick=${showNewer}>${t("chat.pvNewerShow")}</button>${newerNote ? html`<span class="pv-newer-note">${newerNote}</span>` : null}</div>` : null}
|
|
195
230
|
${going ? html`<div class="pv-banner">${P().BANNER}</div>` : null}
|
|
196
231
|
${note && view === "preview" ? html`<div class="pv-note">${note}</div>` : null}
|
|
197
232
|
<div class=${`pv-panel-body${view === "code" ? " show-code" : ""}`}>
|
|
@@ -26,7 +26,7 @@ function view() {
|
|
|
26
26
|
if (!op) return null;
|
|
27
27
|
return {
|
|
28
28
|
id: op.id, phase: op.phase, error: op.error, job: op.job, lostJob: op.lostJob, restart: op.restart,
|
|
29
|
-
steps: op.steps.map(({ label, impact, status, error, pendingId }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null })),
|
|
29
|
+
steps: op.steps.map(({ label, impact, status, error, pendingId, note }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null, ...(note ? { note } : {}) })),
|
|
30
30
|
leftover: op.leftover,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
@@ -138,6 +138,8 @@ async function rotateConnectionSecret(step, o) {
|
|
|
138
138
|
if (status && status.ok) result = status.body || result;
|
|
139
139
|
}
|
|
140
140
|
const ok = result.result === "applied" || result.result === "restart_required";
|
|
141
|
+
// Stored, but no adapter was running to take it (#1519 P1): said as such — not "done", and never "failed".
|
|
142
|
+
if (result.result === "restart_required") { step.note = t("settings.tokenSavedRestart"); publish(); }
|
|
141
143
|
return { ok, status: ok ? 200 : 409, body: ok ? result : { error: result.error || result.result || "apply failed" } };
|
|
142
144
|
}
|
|
143
145
|
|