@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.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/dist/agent-cli.js +6 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +39 -2
- package/dist/backend/claude-code.js +115 -31
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +33 -3
- package/dist/backend/credential-profile.js +107 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +14 -1
- package/dist/channel/adapters/discord.js +42 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +18 -3
- package/dist/channel/adapters/telegram.js +66 -10
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.js +5 -4
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +27 -4
- package/dist/config-validator.js +10 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +108 -0
- package/dist/daemon.js +432 -32
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +29 -7
- package/dist/delivery-outbox.js +134 -16
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -0
- package/dist/fleet-manager.d.ts +81 -4
- package/dist/fleet-manager.js +452 -85
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +73 -26
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +48 -18
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/outbound-handlers.d.ts +63 -0
- package/dist/outbound-handlers.js +121 -46
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -2
- package/dist/outbound-schemas.js +3 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +8 -5
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.js +10 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -2
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/topic-commands.js +9 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +25 -0
- package/dist/ui/chat-store.js +1 -1
- package/dist/ui/chat-strings.js +12 -12
- package/dist/ui/chat-thread.js +48 -11
- package/dist/ui/panel-chat.js +20 -9
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +51 -22
- package/dist/ui/panel-settings.js +18 -9
- package/dist/ui/preview-panel.js +48 -14
- package/dist/ui/preview.js +5 -1
- package/dist/ui/settings-strings.js +6 -2
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +35 -13
- package/dist/ui/shared/app-prefs.js +249 -0
- package/dist/ui/shared/app-shell.js +17 -4
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app.css +41 -7
- package/dist/ui/shared/app.js +6 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +131 -38
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +13 -4
- package/dist/ui/shared/view-strings.js +26 -2
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/view-api.js +5 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +13 -1
- package/dist/web-api.js +218 -18
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +3 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +63 -0
- package/dist/web-prefs.js +263 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-session.js +1 -1
- package/dist/web-session.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/chat-render.js
CHANGED
|
@@ -59,6 +59,21 @@
|
|
|
59
59
|
return '<img class="emoji" src="/ui/img/emoji/' + id + '" alt=":' + name + ':" title=":' + name + ':" decoding="async">';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// #1682: Discord mentions — `<@id>` / `<@!id>` (a user), `<@&id>` (a role), `<#id>` (a channel) — escaped by now (the
|
|
63
|
+
// role's & too), drawn as a pill: the name the page knows (store.mentions(kind, id), inserted escaped), else @user /
|
|
64
|
+
// @role / #channel; never the raw token. The id is the token's own digits (title, data-mention: the page fills a
|
|
65
|
+
// name in later). And a Telegram @username (a letter, then 4–31 word characters; not part of a word, an e-mail
|
|
66
|
+
// or a path) gets the same look — no lookup, no link. It runs after the links (see inline()).
|
|
67
|
+
var MENTION_RE = /<(@!?|@&|#)(\d{15,25})>/g;
|
|
68
|
+
// Not a package or a path either: `@scope/pkg`, `@anthropic-ai/sdk` (followed by - or /) stay text.
|
|
69
|
+
var TG_MENTION_RE = /(^|[^\w@\/.&;])@([A-Za-z][A-Za-z0-9_]{4,31})(?![\w@\/-])/g;
|
|
70
|
+
var MENTION_KIND = { "@": "u", "@!": "u", "@&": "r", "#": "c" };
|
|
71
|
+
var MENTION_UNKNOWN = { u: "@user", r: "@role", c: "#channel" };
|
|
72
|
+
function mentionPill(kind, id, name) {
|
|
73
|
+
var label = name ? (kind === "c" ? "#" : "@") + escapeHtml(name) : MENTION_UNKNOWN[kind];
|
|
74
|
+
return '<span class="mention" data-mention="' + kind + ":" + id + '" title="' + id + '">' + label + "</span>";
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/** Inline Markdown over ALREADY-ESCAPED text. */
|
|
63
78
|
function inline(escaped, store) {
|
|
64
79
|
var s = escaped;
|
|
@@ -66,6 +81,12 @@
|
|
|
66
81
|
s = s.replace(/`([^`\n]+)`/g, function (_m, code) { return stash(store, "<code>" + code + "</code>"); });
|
|
67
82
|
var known = store.emojis;
|
|
68
83
|
if (known) s = s.replace(EMOJI_RE, function (m, _a, name, id) { return known(id) ? stash(store, emojiImg(id, name)) : m; });
|
|
84
|
+
var names = store.mentions;
|
|
85
|
+
s = s.replace(MENTION_RE, function (_m, sigil, id) {
|
|
86
|
+
var kind = MENTION_KIND[sigil];
|
|
87
|
+
var name = names ? names(kind, id) : null;
|
|
88
|
+
return stash(store, mentionPill(kind, id, typeof name === "string" && name ? name : null));
|
|
89
|
+
});
|
|
69
90
|
// [label](url)
|
|
70
91
|
s = s.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function (m, label, url) {
|
|
71
92
|
var href = safeHref(url);
|
|
@@ -76,6 +97,8 @@
|
|
|
76
97
|
var href = safeHref(url);
|
|
77
98
|
return href ? pre + stash(store, link(href, url)) : m;
|
|
78
99
|
});
|
|
100
|
+
// #1682: a Telegram @username — after the links (a link is stashed by now, so an @ inside a URL is never touched).
|
|
101
|
+
s = s.replace(TG_MENTION_RE, function (_m, pre, user) { return pre + stash(store, '<span class="mention tg">@' + user + "</span>"); });
|
|
79
102
|
return emphasis(s);
|
|
80
103
|
}
|
|
81
104
|
|
|
@@ -285,6 +308,8 @@
|
|
|
285
308
|
var store = [];
|
|
286
309
|
// #1630: which server emoji ids the page knows (a function id → boolean); without it every `<:name:id>` stays text.
|
|
287
310
|
store.emojis = opts && typeof opts.emojis === "function" ? opts.emojis : null;
|
|
311
|
+
// #1682: the names the page knows for mentions (a function (kind "u" | "r" | "c", id) → name or null).
|
|
312
|
+
store.mentions = opts && typeof opts.mentions === "function" ? opts.mentions : null;
|
|
288
313
|
var html = "";
|
|
289
314
|
var re = new RegExp(FENCE_RE.source, "g");
|
|
290
315
|
var last = 0;
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -66,7 +66,7 @@ export function createChatStore(deps) {
|
|
|
66
66
|
if (!m || typeof m.instance !== "string") return;
|
|
67
67
|
const before = s.msgs[m.instance];
|
|
68
68
|
s.msgs[m.instance] = R().mergeMessages(before, [m], 500);
|
|
69
|
-
if (m.instance === s.current && s.msgs[m.instance] !== before && !isUser(m, m.instance) && (s.msgs[m.instance] || []).length > (before || []).length) deps.announce(tf("chat.a11yReplied", m.sender));
|
|
69
|
+
if (m.instance === s.current && s.msgs[m.instance] !== before && m.role !== "status" && !isUser(m, m.instance) && (s.msgs[m.instance] || []).length > (before || []).length) deps.announce(tf("chat.a11yReplied", m.sender));
|
|
70
70
|
if (s.msgs[m.instance] !== before) changed(m.instance, "msgs");
|
|
71
71
|
}
|
|
72
72
|
function applyDeliveries(list) {
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -21,8 +21,8 @@ register("chat", {
|
|
|
21
21
|
dropHere: "Drop files to attach them", asText: "As text", pasteAttached: "A long paste ({0} characters) was attached as a text file", pasteKeptAsText: "No room for another attachment: the long paste stays as text",
|
|
22
22
|
send: "Send", composerPlaceholder: "Message {0}…", composerHint: "Enter to send · Shift+Enter for a new line · / for commands · Esc stops the reply", attachTitle: "Attach files (images, PDF, text — or paste / drop them)",
|
|
23
23
|
pvPreview: "Preview", pvStop: "Stop", pvDownload: "Download", pvMenu: "Preview options", pvHtml: "HTML",
|
|
24
|
-
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews
|
|
25
|
-
pvConfirm: "Allow HTML previews on
|
|
24
|
+
pvAllowButton: "Allow previews", pvAllow: "Allow HTML previews — they may be able to send data out", pvDisallow: "Stop allowing HTML previews",
|
|
25
|
+
pvConfirm: "Allow HTML previews?\n\nThis applies on every device where you are signed in. A preview runs the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out, and it can slow or freeze this tab. You still click Preview each time.",
|
|
26
26
|
pvNever: "Never preview HTML on this device", pvNeverUndo: "Let this device preview HTML again", pvStarting: "Starting the preview…",
|
|
27
27
|
pvChanged: "Preview stopped — the message changed. Preview again.", pvTruncated: "Truncated — ask the agent to send it as a .html file.",
|
|
28
28
|
pvLoading: "Reading the file…", pvAttOver: "This file is over 1 MiB; it is not previewed.",
|
|
@@ -37,8 +37,8 @@ register("chat", {
|
|
|
37
37
|
pvNewer: "A newer version is in the reply from {0}.", pvNewerShow: "Show it", pvPanelIdle: "Click Preview to run it here.",
|
|
38
38
|
lbLabel: "Image: {0}", lbLabelNoName: "Image", lbClose: "Close", lbOpenOriginal: "Open original", lbPrev: "Previous image", lbNext: "Next image", lbCount: "{0} / {1}",
|
|
39
39
|
// #1554: preview.js's words (its English is the fallback there), and the server's reasons by code.
|
|
40
|
-
pvWhyOptin: "HTML previews are off
|
|
41
|
-
pvWhyOptinPanel: "HTML previews are off
|
|
40
|
+
pvWhyOptin: "HTML previews are off. Allow them from this card's ⋯ menu or Settings → Preferences.",
|
|
41
|
+
pvWhyOptinPanel: "HTML previews are off.",
|
|
42
42
|
pvWhyNever: "Previews are switched off on this device for this session.",
|
|
43
43
|
pvWhyOrigin: "Previews are off: this page is at {0}, but the fleet believes it is at {1} (a proxy that rewrites Host). Pass the external Host through, or set web.preview_origin.",
|
|
44
44
|
pvUnknownAddress: "an unknown address", pvWhyServer: "Previews are not available here.",
|
|
@@ -55,8 +55,8 @@ register("chat", {
|
|
|
55
55
|
pvServer_sameOrigin: "The preview origin cannot be the dashboard's own.",
|
|
56
56
|
pvServer_publicLink: "Previews are not offered over the public link. Open the dashboard on the machine running AgEnD to preview.",
|
|
57
57
|
pvServer_notOffered: "Previews are not available on this page.",
|
|
58
|
-
previews: "Allow HTML previews
|
|
59
|
-
copy: "Copy", copyMessage: "Copy this reply", copied: "Copied", copyFailed: "Copy failed", wrap: "Wrap", showAll: "Show all {0} lines", showLess: "Show less", jumpLatest: "Latest", newBelow: "{0} new",
|
|
58
|
+
previews: "Allow HTML previews",
|
|
59
|
+
copy: "Copy", copyMessage: "Copy this reply", copied: "Copied", copyFailed: "Copy failed", wrap: "Wrap", showAll: "Show all {0} lines", showMore: "Show more", showLess: "Show less", jumpLatest: "Latest", newBelow: "{0} new",
|
|
60
60
|
disconnected: "disconnected", tick_queued: "Waiting behind another message", tick_processing: "Handed to the agent", tick_delivered: "The agent has it", tick_failed: "Not delivered", tick_cancelled: "Dropped by Stop",
|
|
61
61
|
promptAnswering: "Answering…", promptGone: "This prompt is no longer open", promptFor: "{0}: {1}",
|
|
62
62
|
working: "{0} is working…", stuck: "{0} looks stuck", stopReply: "Stop reply", stopReplyTitle: "Stop the current reply (like Esc) and drop the messages still waiting. The instance keeps running.", stopSent: "Stopped",
|
|
@@ -96,8 +96,8 @@ register("chat", {
|
|
|
96
96
|
dropHere: "放開即可附加檔案", asText: "改為文字", pasteAttached: "貼上的長文字({0} 個字元)已改為文字檔附加", pasteKeptAsText: "附件已滿,貼上的長文字保留為一般文字",
|
|
97
97
|
send: "送出", composerPlaceholder: "傳訊息給 {0}…", composerHint: "Enter 送出 · Shift+Enter 換行 · / 叫出指令 · Esc 中止回覆", attachTitle: "附加檔案(圖片、PDF、文字檔,也可以貼上或拖曳)",
|
|
98
98
|
pvPreview: "預覽", pvStop: "停止", pvDownload: "下載", pvMenu: "預覽選項", pvHtml: "HTML",
|
|
99
|
-
pvAllowButton: "允許預覽", pvAllow: "
|
|
100
|
-
pvConfirm: "
|
|
99
|
+
pvAllowButton: "允許預覽", pvAllow: "允許預覽 HTML(預覽可能把資料傳出去)", pvDisallow: "不再允許預覽 HTML",
|
|
100
|
+
pvConfirm: "要允許預覽 HTML 嗎?\n\n這會套用到你登入的每一台裝置。預覽會在隔離的框架裡執行 agent 的 HTML。它無法使用你的登入,但可能把資料傳出去,也可能讓這個分頁變慢或卡住。每次仍需要你按「預覽」。",
|
|
101
101
|
pvNever: "這台裝置永不預覽 HTML", pvNeverUndo: "讓這台裝置再次可以預覽 HTML", pvStarting: "正在啟動預覽…",
|
|
102
102
|
pvChanged: "預覽已停止——訊息內容變了。請再預覽一次。", pvTruncated: "內容被截斷——請 agent 改用 .html 檔案傳送。",
|
|
103
103
|
pvLoading: "正在讀取檔案…", pvAttOver: "這個檔案超過 1 MiB,不提供預覽。",
|
|
@@ -111,8 +111,8 @@ register("chat", {
|
|
|
111
111
|
pvPage: "頁面", pvCode: "程式碼", pvReload: "重新載入", pvClose: "關閉面板", pvBack: "回到對話", pvResize: "調整預覽面板寬度",
|
|
112
112
|
pvNewer: "{0} 的回覆裡有較新的版本。", pvNewerShow: "顯示", pvPanelIdle: "按「預覽」在這裡執行。",
|
|
113
113
|
lbLabel: "圖片:{0}", lbLabelNoName: "圖片", lbClose: "關閉", lbOpenOriginal: "開啟原圖", lbPrev: "上一張", lbNext: "下一張", lbCount: "{0} / {1}",
|
|
114
|
-
pvWhyOptin: "
|
|
115
|
-
pvWhyOptinPanel: "
|
|
114
|
+
pvWhyOptin: "還沒允許 HTML 預覽。可從這張卡片的 ⋯ 選單或「設定 → 偏好設定」允許。",
|
|
115
|
+
pvWhyOptinPanel: "還沒允許 HTML 預覽。",
|
|
116
116
|
pvWhyNever: "這台裝置在這個工作階段已關閉預覽。",
|
|
117
117
|
pvWhyOrigin: "預覽已關閉:這個頁面位於 {0},但艦隊認為它位於 {1}(有會改寫 Host 的代理)。請讓外部 Host 原樣傳入,或設定 web.preview_origin。",
|
|
118
118
|
pvUnknownAddress: "未知的網址", pvWhyServer: "這裡無法使用預覽。",
|
|
@@ -129,8 +129,8 @@ register("chat", {
|
|
|
129
129
|
pvServer_sameOrigin: "預覽的來源不能和儀表板相同。",
|
|
130
130
|
pvServer_publicLink: "公開連結不提供預覽。要預覽,請在執行 AgEnD 的那台機器上開啟儀表板。",
|
|
131
131
|
pvServer_notOffered: "這個頁面不提供預覽。",
|
|
132
|
-
previews: "
|
|
133
|
-
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
132
|
+
previews: "允許預覽 HTML",
|
|
133
|
+
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showMore: "顯示更多", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
134
134
|
disconnected: "已斷線", tick_queued: "排在前一則訊息之後", tick_processing: "已交給 agent", tick_delivered: "agent 已收到", tick_failed: "未送達", tick_cancelled: "已被中止取消",
|
|
135
135
|
promptAnswering: "回覆中…", promptGone: "這個提示已經結束", promptFor: "{0}:{1}",
|
|
136
136
|
working: "{0} 處理中…", stuck: "{0} 似乎卡住了", stopReply: "中止回覆", stopReplyTitle: "中止目前的回覆(同 Esc),並取消還在排隊的訊息。instance 會繼續運行。", stopSent: "已中止",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -22,6 +22,9 @@ export const TEXT_PREVIEW_NODES = 4000;
|
|
|
22
22
|
/** A csv/tsv wider than this is shown as text, as Markdown tables are capped at 20 columns. */
|
|
23
23
|
export const TEXT_PREVIEW_COLUMNS = 50;
|
|
24
24
|
const P = () => globalThis.AgendPreview;
|
|
25
|
+
/** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
|
|
26
|
+
export const STATUS_FOLD_LINES = 6;
|
|
27
|
+
export const STATUS_FOLD_CHARS = 600;
|
|
25
28
|
const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
26
29
|
|
|
27
30
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
@@ -31,7 +34,7 @@ export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
|
31
34
|
/**
|
|
32
35
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
33
36
|
* setPreviewOptIn(on), copyText(text) → Promise<boolean>, download(code), panel? }. panel (#1481): { shown() → the card
|
|
34
|
-
* key the side panel shows, or null; open(spec, { run }) } — without it a card has no "Open in panel".
|
|
37
|
+
* key the side panel shows, or null; open(spec, { run }); version(); claim(); claims() } — without it a card has no "Open in panel".
|
|
35
38
|
*/
|
|
36
39
|
export function createThread(list, scroller, opts) {
|
|
37
40
|
const nodes = new Map(); // key → { html, node }
|
|
@@ -50,13 +53,29 @@ export function createThread(list, scroller, opts) {
|
|
|
50
53
|
const TICKS = () => ({ queued: tr("chat.tick_queued"), processing: tr("chat.tick_processing"), delivered: tr("chat.tick_delivered"), failed: tr("chat.tick_failed"), cancelled: tr("chat.tick_cancelled") });
|
|
51
54
|
|
|
52
55
|
function msgHtml(x) {
|
|
53
|
-
const u = opts.isUser(x);
|
|
54
56
|
const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
57
|
+
if (x.role === "status") return statusHtml(x, time);
|
|
58
|
+
const u = opts.isUser(x);
|
|
55
59
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
56
60
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
57
61
|
const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
58
62
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
59
|
-
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span><span class="time">${time}</span>${ticks}</div><div class="body"><div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
63
|
+
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, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
67
|
+
* a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
|
|
68
|
+
* start with "agend". A long one is folded until it is opened.
|
|
69
|
+
*/
|
|
70
|
+
function statusHtml(x, time) {
|
|
71
|
+
const text = String(x.text == null ? "" : x.text);
|
|
72
|
+
const lines = R().lineCount(text);
|
|
73
|
+
const manyLines = lines > STATUS_FOLD_LINES;
|
|
74
|
+
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
75
|
+
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
76
|
+
: "";
|
|
77
|
+
const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
78
|
+
return `<div class="msg status${more ? " folded" : ""}"><div class="meta"><span class="time">${time}</span></div><div class="body"><div class="st-text">${escAttr(text)}</div>${more}</div>${tools}</div>`;
|
|
60
79
|
}
|
|
61
80
|
/**
|
|
62
81
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -299,14 +318,32 @@ export function createThread(list, scroller, opts) {
|
|
|
299
318
|
};
|
|
300
319
|
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
301
320
|
// Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
|
|
302
|
-
|
|
321
|
+
// A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
|
|
322
|
+
// own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
|
|
323
|
+
// Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
|
|
324
|
+
run.onclick = () => {
|
|
325
|
+
if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
|
|
326
|
+
const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
|
|
327
|
+
withCode((c) => {
|
|
328
|
+
if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
|
|
329
|
+
P().start(key, holder, c, ui);
|
|
330
|
+
});
|
|
331
|
+
};
|
|
303
332
|
stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
|
|
304
|
-
//
|
|
305
|
-
//
|
|
333
|
+
// #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
|
|
334
|
+
// starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
|
|
335
|
+
// previews is offered Allow first (and allowing runs it); where previews are off, nothing runs.
|
|
336
|
+
// An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
|
|
337
|
+
// wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
|
|
338
|
+
// a late read never undoes a newer click.
|
|
306
339
|
if (toPanel) toPanel.onclick = () => {
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
340
|
+
if (reading) return; // this card already reading: that click stays the one
|
|
341
|
+
if (P().running(key)) P().stop(key, "stopped", "");
|
|
342
|
+
const at = panel.claim(), started = P().startCount();
|
|
343
|
+
withCode((c) => {
|
|
344
|
+
if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
|
|
345
|
+
panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: true });
|
|
346
|
+
});
|
|
310
347
|
};
|
|
311
348
|
if (dl) dl.onclick = () => opts.download(code);
|
|
312
349
|
ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
|
|
@@ -426,9 +463,9 @@ export function createThread(list, scroller, opts) {
|
|
|
426
463
|
if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
|
|
427
464
|
else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
|
|
428
465
|
else if (act === "toggleFold") {
|
|
429
|
-
const box = el.closest(".codeblock"); if (!box) return;
|
|
466
|
+
const box = el.closest(".codeblock, .msg.status"); if (!box) return;
|
|
430
467
|
const folded = box.classList.toggle("folded");
|
|
431
|
-
el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.
|
|
468
|
+
el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
|
|
432
469
|
} else if (act === "toggleWrap") opts.toggleWrap();
|
|
433
470
|
else if (act === "replyButton") {
|
|
434
471
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -22,12 +22,14 @@ import { Empty, Skeleton } from "/assets/ui-states.js";
|
|
|
22
22
|
import { Icon } from "/assets/ui-icons.js";
|
|
23
23
|
import { toast } from "/assets/ui-toast.js";
|
|
24
24
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
25
|
+
import { savePref } from "/assets/app-prefs.js";
|
|
25
26
|
import "./chat-strings.js";
|
|
26
27
|
import { confirmedWrite } from "./settings-confirm.js";
|
|
27
28
|
import { createChatStore } from "./chat-store.js";
|
|
28
29
|
import { createThread, msgKey } from "./chat-thread.js";
|
|
29
30
|
import { createEmojiIndex } from "./chat-emojis.js";
|
|
30
|
-
import {
|
|
31
|
+
import { createMentionIndex } from "./chat-mentions.js";
|
|
32
|
+
import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
|
|
31
33
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
32
34
|
import { installTour, refreshTourSpot } from "./chat-tour.js";
|
|
33
35
|
import { FirstRunCard } from "./first-run.js";
|
|
@@ -44,6 +46,7 @@ export let store = null;
|
|
|
44
46
|
let announceTimer = null;
|
|
45
47
|
/** #1630: which Discord server emoji this page may draw (created with the store, in boot). */
|
|
46
48
|
let emojiIndex = null;
|
|
49
|
+
let mentionIndex = null; // #1682: the names of the mentions shown
|
|
47
50
|
function announce(text) {
|
|
48
51
|
let el = document.getElementById("announcer");
|
|
49
52
|
if (!el) { el = document.createElement("div"); el.id = "announcer"; el.className = "sr-only"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); el.setAttribute("aria-atomic", "true"); document.body.appendChild(el); }
|
|
@@ -55,11 +58,14 @@ function announce(text) {
|
|
|
55
58
|
|
|
56
59
|
/**
|
|
57
60
|
* This device's preview opt-in: asks once (the app's dialog) and says what it means; turning it off stops every
|
|
58
|
-
* running preview. Resolves to the choice in force.
|
|
61
|
+
* running preview. Resolves to the choice in force. commit(apply): runs `apply` (the change itself) — the panel's Allow
|
|
62
|
+
* wraps it to know which change of the choice is its own (#1704).
|
|
59
63
|
*/
|
|
60
|
-
export async function setPreviewOptIn(on) {
|
|
64
|
+
export async function setPreviewOptIn(on, commit = (apply) => apply()) {
|
|
61
65
|
if (on && !P().optedIn() && !(await confirmDialog({ message: t("chat.pvConfirm"), confirmLabel: t("chat.pvAllowButton") }))) on = false;
|
|
62
|
-
P().setOptIn(on);
|
|
66
|
+
commit(() => P().setOptIn(on));
|
|
67
|
+
// #1665: kept on the host for every device (only a signed-in page writes it; never the public link).
|
|
68
|
+
savePref("preview.optIn", P().optedIn());
|
|
63
69
|
return P().optedIn();
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -70,6 +76,7 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
70
76
|
fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), confirm: confirmDialog, ...deps,
|
|
71
77
|
});
|
|
72
78
|
emojiIndex = createEmojiIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
|
|
79
|
+
mentionIndex = createMentionIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
|
|
73
80
|
store.attach(stream);
|
|
74
81
|
// #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
|
|
75
82
|
// belong to the app, once (they are never per mount).
|
|
@@ -194,6 +201,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
|
194
201
|
try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
|
|
195
202
|
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
196
203
|
if (r && r.error) toast(r.error, false);
|
|
204
|
+
else if (r && r.held) toast(r.message);
|
|
197
205
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
198
206
|
};
|
|
199
207
|
const items = [
|
|
@@ -218,7 +226,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
|
218
226
|
<${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
|
|
219
227
|
</div>
|
|
220
228
|
<${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
|
|
221
|
-
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
|
|
229
|
+
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
|
|
222
230
|
</div>
|
|
223
231
|
<${LightboxHost} />
|
|
224
232
|
<div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
|
|
@@ -237,14 +245,17 @@ function Thread({ name, th }) {
|
|
|
237
245
|
setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
|
|
238
246
|
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
239
247
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
240
|
-
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
248
|
+
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
|
|
241
249
|
openImage: openLightbox, // an image, full size, in the page
|
|
242
250
|
emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
|
|
251
|
+
mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
|
|
243
252
|
});
|
|
244
253
|
th.current = thread;
|
|
245
254
|
// #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
|
|
246
|
-
|
|
255
|
+
// #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
|
|
256
|
+
const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
|
|
247
257
|
const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
|
|
258
|
+
const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
|
|
248
259
|
thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
|
|
249
260
|
wantEmojis(store.state.msgs[name]);
|
|
250
261
|
const off = store.subscribe((instance, kind) => {
|
|
@@ -259,7 +270,7 @@ function Thread({ name, th }) {
|
|
|
259
270
|
sc.addEventListener("scroll", onScroll, { passive: true });
|
|
260
271
|
ls.addEventListener("click", thread.onClick);
|
|
261
272
|
return () => {
|
|
262
|
-
off(); offPv(); offPanel(); offEmojis();
|
|
273
|
+
off(); offPv(); offPanel(); offEmojis(); offMentions();
|
|
263
274
|
sc.removeEventListener("scroll", onScroll);
|
|
264
275
|
ls.removeEventListener("click", thread.onClick);
|
|
265
276
|
thread.dispose(); // the whole thread goes: every preview in it stops first
|
|
@@ -476,7 +487,7 @@ function DeleteDialog({ name, onClose }) {
|
|
|
476
487
|
<button type="button" class="btn btn-danger" disabled=${typed !== want || busy} onClick=${go}>${t("chat.deleteGo")}</button>`}>
|
|
477
488
|
<p>${t("chat.deleteHelp", name)}</p>
|
|
478
489
|
<label class="field"><span class="sr-only">${t("chat.deleteHelp", name)}</span>
|
|
479
|
-
<input type="text" value=${typed} autocomplete="off" spellcheck
|
|
490
|
+
<input type="text" value=${typed} autocomplete="off" spellcheck=${false} placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
|
|
480
491
|
onKeyDown=${(e) => { if (e.key === "Enter") go(); }} /></label>
|
|
481
492
|
</${Dialog}>`;
|
|
482
493
|
}
|
package/dist/ui/panel-details.js
CHANGED
|
@@ -73,11 +73,13 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
|
|
|
73
73
|
try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
|
|
74
74
|
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
75
75
|
if (r && r.error) toast(r.error, false);
|
|
76
|
+
else if (r && r.held) toast(r.message);
|
|
76
77
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
77
78
|
});
|
|
78
79
|
const pauseWake = (verb) => act(verb, async () => {
|
|
79
80
|
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${verb}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
|
|
80
81
|
if (!res.ok) toast((res.body && res.body.error) || t("chat.dActionFailed"), false);
|
|
82
|
+
else if (res.body && res.body.held) toast(res.body.message);
|
|
81
83
|
else toast(t(verb === "pause" ? "chat.dPaused" : "chat.dWoken", name));
|
|
82
84
|
});
|
|
83
85
|
// A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'
|
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -23,6 +23,8 @@ import { CacheReport } from "./fleet-cache.js";
|
|
|
23
23
|
|
|
24
24
|
register("fleet", {
|
|
25
25
|
en: {
|
|
26
|
+
hostMemory_elevated: "Host memory pressure: elevated — new agents start one at a time, at least 5 s apart",
|
|
27
|
+
hostMemory_critical: "Host memory pressure: critical — new agents wait for memory to recover",
|
|
26
28
|
title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
|
|
27
29
|
newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
|
|
28
30
|
noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
|
|
@@ -33,7 +35,8 @@ register("fleet", {
|
|
|
33
35
|
cron: "Cron expression", message: "Message", target: "Target instance", label: "Label", teamName: "Team name", members: "Members",
|
|
34
36
|
titleRequired: "Title required", scheduleFieldsRequired: "Cron, message, and target required", teamFieldsRequired: "Name and at least one member required",
|
|
35
37
|
deleteSchedule: "Delete this schedule?", deleteTeam: "Delete team “{0}”?", taskCreated: "Task created", scheduleCreated: "Schedule created", scheduleDeleted: "Schedule deleted",
|
|
36
|
-
teamCreated: "Team created", teamDeleted: "Team deleted",
|
|
38
|
+
teamCreated: "Team created", teamDeleted: "Team deleted", teamUpdated: "Team updated", edit: "Edit", editTeam: "Edit team",
|
|
39
|
+
teamNoRename: "Renaming isn't supported — create a new team and delete this one.", taskDone: "Task updated", configSaved: "Config saved", configRestart: " (restart the fleet to apply channel changes)",
|
|
37
40
|
channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
|
|
38
41
|
allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
|
|
39
42
|
saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
|
|
@@ -43,6 +46,8 @@ register("fleet", {
|
|
|
43
46
|
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
44
47
|
},
|
|
45
48
|
"zh-TW": {
|
|
49
|
+
hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
|
|
50
|
+
hostMemory_critical: "主機記憶體壓力嚴重:新的 agent 會等記憶體恢復再啟動",
|
|
46
51
|
title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
|
|
47
52
|
newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
|
|
48
53
|
noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
|
|
@@ -53,7 +58,8 @@ register("fleet", {
|
|
|
53
58
|
cron: "Cron 表示式", message: "訊息", target: "目標 instance", label: "標籤", teamName: "Team 名稱", members: "成員",
|
|
54
59
|
titleRequired: "必須填寫標題", scheduleFieldsRequired: "必須填寫 Cron、訊息與目標 instance", teamFieldsRequired: "必須填寫名稱並選擇至少一名成員",
|
|
55
60
|
deleteSchedule: "確定刪除此排程嗎?", deleteTeam: "確定刪除 team「{0}」嗎?", taskCreated: "Task 已建立", scheduleCreated: "排程已建立", scheduleDeleted: "排程已刪除",
|
|
56
|
-
teamCreated: "Team 已建立", teamDeleted: "Team 已刪除",
|
|
61
|
+
teamCreated: "Team 已建立", teamDeleted: "Team 已刪除", teamUpdated: "Team 已更新", edit: "編輯", editTeam: "編輯 team",
|
|
62
|
+
teamNoRename: "不支援重新命名:請建立新的 team,再刪除這個。", taskDone: "Task 已更新", configSaved: "設定已儲存", configRestart: "(請重新啟動 fleet 以套用頻道變更)",
|
|
57
63
|
channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
|
|
58
64
|
allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
|
|
59
65
|
saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
|
|
@@ -104,8 +110,11 @@ export function FleetPanel({ route, navKey }) {
|
|
|
104
110
|
const tab = route.tab;
|
|
105
111
|
useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
|
|
106
112
|
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
|
|
113
|
+
// #1685: host memory pressure slowing or holding new agents — shown here, where macOS's routine "elevated" is not a chat notice.
|
|
114
|
+
const { hostMemory } = useStore(appStore);
|
|
115
|
+
const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
|
|
107
116
|
return html`<div class="panel p-fleet">
|
|
108
|
-
<${PanelHeader} title=${t("fleet.title")}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
|
|
117
|
+
<${PanelHeader} title=${t("fleet.title")} sub=${sub}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
|
|
109
118
|
<nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
|
|
110
119
|
aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
|
|
111
120
|
<div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
@@ -167,9 +176,13 @@ function Schedules({ lease }) {
|
|
|
167
176
|
${creating ? html`<${CreateSchedule} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
/** #1689: the public link changes no team (the server refuses it): it lists them only. */
|
|
180
|
+
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
181
|
+
|
|
170
182
|
function Teams({ lease }) {
|
|
171
183
|
const [version, setVersion] = useState(0);
|
|
172
184
|
const [creating, setCreating] = useState(false);
|
|
185
|
+
const [editing, setEditing] = useState(null); // #1689: { name, members, description } of the team being edited
|
|
173
186
|
const state = useLoad(lease, "/ui/teams", version);
|
|
174
187
|
async function del(name) {
|
|
175
188
|
if (!(await confirmDialog({ message: t("fleet.deleteTeam", name), confirmLabel: t("fleet.delete"), danger: true }))) return;
|
|
@@ -179,14 +192,17 @@ function Teams({ lease }) {
|
|
|
179
192
|
}
|
|
180
193
|
return html`<${Loaded} state=${state}>${(d) => {
|
|
181
194
|
const entries = Object.entries(d.teams || {});
|
|
182
|
-
|
|
195
|
+
const writable = !publicLink();
|
|
196
|
+
return html`<${ListHead} count=${entries.length === 1 ? t("fleet.countTeam") : t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${writable ? { onClick: () => setCreating(true) } : null} />
|
|
183
197
|
${entries.length ? html`<ul class="rows">${entries.map(([name, team]) => html`<li key=${name} class="row-item">
|
|
184
198
|
<span class="grow strong">${name}</span><span class="muted">${(team.members || []).join(", ")}</span>
|
|
185
|
-
|
|
199
|
+
${writable ? html`<button type="button" class="btn btn-ghost btn-sm team-edit" onClick=${() => setEditing({ name, members: [...(team.members || [])], description: team.description || "" })}>${t("fleet.edit")}</button>
|
|
200
|
+
<button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(name)}>${t("fleet.delete")}</button>` : null}</li>`)}</ul>`
|
|
186
201
|
: html`<${Empty} icon="team" title=${t("fleet.noTeams")} hint=${t("fleet.noTeamsHint")} />`}
|
|
187
202
|
`;
|
|
188
203
|
}}</${Loaded}>
|
|
189
|
-
${creating ? html`<${CreateTeam} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}
|
|
204
|
+
${creating ? html`<${CreateTeam} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}
|
|
205
|
+
${editing ? html`<${CreateTeam} team=${editing} onClose=${() => setEditing(null)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
/** #1389: the org chart — its structure read once per opening, its live state from the app store (fleet-org.js). */
|
|
@@ -286,7 +302,7 @@ export function FormDialog({ title, onClose, submit, children, ready = true }) {
|
|
|
286
302
|
// the dialog goes at once and the shell follows the request. From then on the dialog is done: the decision is
|
|
287
303
|
// said in a toast, and nothing here closes, refreshes or acts again.
|
|
288
304
|
try {
|
|
289
|
-
const res = await confirmedWrite(submit.path, { method: "POST", body, label: title,
|
|
305
|
+
const res = await confirmedWrite(submit.path, { method: submit.method || "POST", body, label: title,
|
|
290
306
|
onPending: () => { handedOver = true; toast(t("app.pendingSent")); if (lease.current()) { setBusy(false); onClose(); } } });
|
|
291
307
|
r = res.ok ? res.body || {} : { error: (res.body && res.body.error) || `HTTP ${res.status}` };
|
|
292
308
|
} catch (err) { r = { error: err.message }; }
|
|
@@ -299,7 +315,7 @@ export function FormDialog({ title, onClose, submit, children, ready = true }) {
|
|
|
299
315
|
}
|
|
300
316
|
return html`<${Dialog} title=${title} onClose=${onClose} busy=${busy}
|
|
301
317
|
actions=${html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${t("fleet.cancel")}</button>
|
|
302
|
-
<button type="button" class="btn btn-primary" onClick=${go} disabled=${busy || !ready}>${busy ? t("fleet.creating") : t("fleet.create")}</button>`}>
|
|
318
|
+
<button type="button" class="btn btn-primary" onClick=${go} disabled=${busy || !ready}>${busy ? t("fleet.creating") : submit.label || t("fleet.create")}</button>`}>
|
|
303
319
|
<form class="form" onSubmit=${(e) => { e.preventDefault(); go(); }}><fieldset class="form" disabled=${busy}>${children}</fieldset></form></${Dialog}>`;
|
|
304
320
|
}
|
|
305
321
|
const field = (label, input) => html`<label class="field"><span>${label}</span>${input}</label>`;
|
|
@@ -342,21 +358,34 @@ function CreateSchedule({ onClose, onDone }) {
|
|
|
342
358
|
</${FormDialog}>`;
|
|
343
359
|
}
|
|
344
360
|
|
|
345
|
-
|
|
361
|
+
/**
|
|
362
|
+
* New team, or (#1689) `team` given: edit that one — the same form, filled in from it: its members ticked (one no longer
|
|
363
|
+
* in the fleet listed too, so it can be taken out), its description; the name is not editable (a team is not renamed).
|
|
364
|
+
* Saved with PATCH /ui/teams/<name> (the update_team path on the host).
|
|
365
|
+
*/
|
|
366
|
+
function CreateTeam({ onClose, onDone, team = null }) {
|
|
346
367
|
const { instances } = useStore(appStore);
|
|
347
|
-
const [f, setF] = useState({ name: "", desc: "", members: [] });
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
const [f, setF] = useState(team ? { name: team.name, desc: team.description || "", members: [...team.members] } : { name: "", desc: "", members: [] });
|
|
369
|
+
// From the state as it is, not this render's copy: two ticks before the next render must both count.
|
|
370
|
+
const toggle = (n) => setF((cur) => ({ ...cur, members: cur.members.includes(n) ? cur.members.filter(x => x !== n) : [...cur.members, n] }));
|
|
371
|
+
const names = [...new Set([...instances.map(i => i.name), ...(team ? team.members : [])])];
|
|
372
|
+
const submit = team
|
|
373
|
+
? { path: `/ui/teams/${encodeURIComponent(team.name)}`, method: "PATCH", label: t("fleet.save"), done: t("fleet.teamUpdated"), after: onDone, collect: () => {
|
|
374
|
+
if (!f.members.length) { toast(t("fleet.teamFieldsRequired"), false); return null; }
|
|
375
|
+
return { members: f.members, description: f.desc.trim() };
|
|
376
|
+
} }
|
|
377
|
+
: { path: "/ui/teams", done: t("fleet.teamCreated"), after: onDone, collect: () => {
|
|
378
|
+
if (!f.name.trim() || !f.members.length) { toast(t("fleet.teamFieldsRequired"), false); return null; }
|
|
379
|
+
const body = { name: f.name.trim(), members: f.members };
|
|
380
|
+
if (f.desc.trim()) body.description = f.desc.trim();
|
|
381
|
+
return body;
|
|
382
|
+
} };
|
|
383
|
+
return html`<${FormDialog} title=${team ? t("fleet.editTeam") : t("fleet.newTeam")} onClose=${onClose} submit=${submit}>
|
|
384
|
+
${field(`${t("fleet.teamName")} *`, html`<input value=${f.name} readOnly=${!!team} aria-readonly=${team ? "true" : undefined} onInput=${(e) => { const v = e.target.value; setF((cur) => ({ ...cur, name: v })); }} />`)}
|
|
385
|
+
${team ? html`<p class="note">${t("fleet.teamNoRename")}</p>` : null}
|
|
386
|
+
${field(t("fleet.taskDesc"), html`<input value=${f.desc} onInput=${(e) => { const v = e.target.value; setF((cur) => ({ ...cur, desc: v })); }} />`)}
|
|
387
|
+
<fieldset class="field"><legend>${t("fleet.members")} *</legend><div class="checks">${names.map(n => html`<label key=${n} class="check">
|
|
388
|
+
<input type="checkbox" checked=${f.members.includes(n)} onChange=${() => toggle(n)} />${n}</label>`)}</div></fieldset>
|
|
360
389
|
</${FormDialog}>`;
|
|
361
390
|
}
|
|
362
391
|
|