@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.4
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-general.d.ts +15 -0
- package/dist/discord-general.js +24 -0
- package/dist/discord-general.js.map +1 -0
- 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 +610 -142
- 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 +88 -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 +75 -0
- package/dist/public-link-progress.js +204 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +4 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -2
- package/dist/public-web-link.js +89 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +198 -63
- 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 +48 -84
- 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 +42 -0
- package/dist/tunnel/cloudflared-install.js +194 -9
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/deb-extract.d.ts +9 -0
- package/dist/tunnel/deb-extract.js +94 -0
- package/dist/tunnel/deb-extract.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/app.html +3 -0
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +45 -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/icons/apple-touch-icon.png +0 -0
- package/dist/ui/icons/favicon.ico +0 -0
- package/dist/ui/icons/favicon.svg +8 -0
- package/dist/ui/panel-chat.js +16 -7
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +47 -7
- package/dist/ui/preview.js +36 -13
- 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/ui/signin.html +3 -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 +53 -17
- 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-icons.d.ts +21 -0
- package/dist/web-icons.js +38 -0
- package/dist/web-icons.js.map +1 -0
- package/dist/web-preview.d.ts +9 -0
- package/dist/web-preview.js +15 -9
- package/dist/web-preview.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
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<title>AgEnD — Ag</title>
|
|
3
|
+
<rect x="1" y="1" width="62" height="62" rx="12" fill="#e5eaf0" stroke="#617386" stroke-width="2"/>
|
|
4
|
+
<g transform="translate(0 -2)"><g fill="#14273d" fill-rule="evenodd">
|
|
5
|
+
<path d="M8.5 43 17 18h6l8.5 25H26l-2-6h-8l-2 6Zm9-10.5h5l-2.5-8Z"/>
|
|
6
|
+
<path transform="translate(2 0)" d="M49.5 27v18.8c0 5-3.1 7.2-8.2 7.2h-6.8v-4.8H41c2.6 0 3.7-.9 3.7-3.2v-3.3c-1.3 1.5-3.1 2.3-5.6 2.3-5.8 0-9-3.7-9-8.9 0-5.1 3.2-8.9 9-8.9 2.5 0 4.3.8 5.6 2.3V27Zm-9.7 3.8c-3 0-4.6 1.8-4.6 4.3s1.6 4.3 4.6 4.3 4.6-1.8 4.6-4.3-1.6-4.3-4.6-4.3Z"/>
|
|
7
|
+
</g></g>
|
|
8
|
+
</svg>
|
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;
|
|
@@ -63,7 +64,8 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
63
64
|
store.attach(stream);
|
|
64
65
|
// #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
|
|
65
66
|
// belong to the app, once (they are never per mount).
|
|
66
|
-
|
|
67
|
+
// #1554: its words in the page's language (chat.pv*); a key the dictionary does not have keeps preview.js's English.
|
|
68
|
+
if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
|
|
67
69
|
addFooterItem("previews", PreviewOptIn);
|
|
68
70
|
addFooterItem("tour", TourButton);
|
|
69
71
|
installTour();
|
|
@@ -99,9 +101,9 @@ async function copyText(text) {
|
|
|
99
101
|
return ok;
|
|
100
102
|
}
|
|
101
103
|
/** The HTML as a file: a blob of the source as application/octet-stream, saved as reply.html — never opened. */
|
|
102
|
-
function downloadHtml(code) {
|
|
104
|
+
function downloadHtml(code, name) {
|
|
103
105
|
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";
|
|
106
|
+
const a = document.createElement("a"); a.href = url; a.download = typeof name === "string" && name.trim() ? name.trim() : "reply.html"; a.className = "offscreen";
|
|
105
107
|
document.body.appendChild(a); a.click(); a.remove();
|
|
106
108
|
URL.revokeObjectURL(url);
|
|
107
109
|
}
|
|
@@ -127,9 +129,14 @@ export function ChatPanel({ route, navKey }) {
|
|
|
127
129
|
}, [name, !!inst]);
|
|
128
130
|
useEffect(() => { setTitle(name || t("app.chat")); }, [name, navKey]);
|
|
129
131
|
if (!name) {
|
|
132
|
+
// #1519 P7: a web-only fleet starts here with the first-run card; with no agent at all, the way to make one.
|
|
133
|
+
const none = app.ready && !app.instances.length;
|
|
130
134
|
return html`<div class="panel p-chat"><${PanelHeader} title=${t("app.chat")} />
|
|
131
|
-
<div class="panel-body center"
|
|
132
|
-
|
|
135
|
+
<div class="panel-body center"><div class="first-run-stack"><${FirstRunCard} />
|
|
136
|
+
${none ? html`<${Empty} icon="bot" title=${t("app.noInstances")} hint=${t("app.noInstancesHint")}
|
|
137
|
+
action=${html`<button type="button" class="btn btn-primary" onClick=${requestNewInstance}><${Icon} name="plus" size=${16} />${t("app.newInstance")}</button>`} />`
|
|
138
|
+
: html`<${Empty} icon="chat" title=${t("chat.pick")} hint=${t("chat.pickHint")}
|
|
139
|
+
action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} />`}</div></div></div>`;
|
|
133
140
|
}
|
|
134
141
|
if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
|
|
135
142
|
if (!inst) return html`<${NotFound} name=${name} />`;
|
|
@@ -204,7 +211,7 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
204
211
|
<${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
|
|
205
212
|
</div>
|
|
206
213
|
<${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
|
|
207
|
-
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} />
|
|
214
|
+
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
|
|
208
215
|
</div>
|
|
209
216
|
<div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
|
|
210
217
|
${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
|
|
@@ -221,6 +228,8 @@ function Thread({ name, th }) {
|
|
|
221
228
|
const thread = createThread(list.current, scroller.current, {
|
|
222
229
|
t, tf: t, isUser: (x) => store.isUser(x, name), onJump: setJump, onEmpty: setEmpty,
|
|
223
230
|
setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
|
|
231
|
+
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
232
|
+
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
224
233
|
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
225
234
|
});
|
|
226
235
|
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}
|