@songsid/agend 2.2.0-beta.16 → 2.2.0-beta.18
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 +19 -18
- package/README.zh-TW.md +19 -18
- package/dist/agent-whats-new.js +3 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/backend/antigravity.d.ts +25 -0
- package/dist/backend/antigravity.js +94 -9
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/kiro.d.ts +2 -0
- package/dist/backend/kiro.js +4 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +5 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/compact-watch.d.ts +9 -1
- package/dist/compact-watch.js +47 -6
- package/dist/compact-watch.js.map +1 -1
- package/dist/config-validator.js +26 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +66 -6
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.js +78 -23
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/locale.js +8 -4
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +6 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -2
- package/dist/topic-commands.js +18 -6
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/ui/chat-render.js +31 -6
- package/dist/ui/chat-store.js +5 -5
- package/dist/ui/chat-strings.js +6 -2
- package/dist/ui/chat-thread.js +90 -14
- package/dist/ui/panel-chat.js +1 -1
- package/dist/ui/panel-settings.js +1 -1
- package/dist/ui/shared/app.css +7 -0
- package/dist/web-api.d.ts +4 -0
- package/dist/web-api.js +135 -43
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +14 -0
- package/dist/web-chat-history.js +18 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-upload.d.ts +112 -1
- package/dist/web-upload.js +237 -36
- package/dist/web-upload.js.map +1 -1
- package/package.json +3 -3
package/dist/types.d.ts
CHANGED
|
@@ -345,6 +345,14 @@ export interface WebConfig {
|
|
|
345
345
|
echo_to_channel?: boolean;
|
|
346
346
|
/** Show the AI subscription usage panel on /view and serve /api/ai-usage (default true). */
|
|
347
347
|
usage_panel?: boolean;
|
|
348
|
+
/**
|
|
349
|
+
* #1937: the largest file the web chat takes, in MB (whole number, 1–500; default 50). Never more than
|
|
350
|
+
* upload_max_total_mb; over the public link a file is at most 95 MB whatever this says (the quick tunnel's limit).
|
|
351
|
+
* Read on each upload: `agend reload` applies a change; an open page uses the new limits after it reloads.
|
|
352
|
+
*/
|
|
353
|
+
upload_max_file_mb?: number;
|
|
354
|
+
/** #1937: all files on one web chat message together, in MB (whole number, 1–500; default 100). At most 5 files. */
|
|
355
|
+
upload_max_total_mb?: number;
|
|
348
356
|
/**
|
|
349
357
|
* #1218: the instance Log tab (紀錄) — each agent's steps (prompts, replies, tool calls and results, errors) read on
|
|
350
358
|
* demand from its own transcript, secrets masked. Default false: off, it does no work at all. Turning it on is an
|
package/dist/ui/chat-render.js
CHANGED
|
@@ -489,8 +489,32 @@
|
|
|
489
489
|
return draft ? failedText + "\n" + draft : failedText;
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
/** The upload limits the server enforces (web-upload.ts UPLOAD_LIMITS)
|
|
493
|
-
var FILE_LIMITS = { maxFileBytes:
|
|
492
|
+
/** The defaults of the upload limits the server enforces (web-upload.ts UPLOAD_LIMITS), for a page that was given none. */
|
|
493
|
+
var FILE_LIMITS = { maxFileBytes: 50 * 1024 * 1024, maxFiles: 5, maxTotalBytes: 100 * 1024 * 1024 };
|
|
494
|
+
/**
|
|
495
|
+
* #1937: the limits in force for this page — the server's (web.upload_max_file_mb / _total_mb, and the public link's
|
|
496
|
+
* own cap), on <body data-upload-limits>. Checked here first only to say so early: the server checks every upload.
|
|
497
|
+
*/
|
|
498
|
+
function fileLimits() {
|
|
499
|
+
try {
|
|
500
|
+
var raw = JSON.parse(document.body.dataset.uploadLimits || "null");
|
|
501
|
+
var ok = function (n) { return typeof n === "number" && isFinite(n) && n > 0; };
|
|
502
|
+
if (raw && ok(raw.maxFileBytes) && ok(raw.maxFiles) && ok(raw.maxTotalBytes)) {
|
|
503
|
+
return { maxFileBytes: raw.maxFileBytes, maxFiles: raw.maxFiles, maxTotalBytes: raw.maxTotalBytes, publicLinkCapped: raw.publicLinkCapped === true };
|
|
504
|
+
}
|
|
505
|
+
} catch (e) { /* no page, or no limits on it */ }
|
|
506
|
+
return { maxFileBytes: FILE_LIMITS.maxFileBytes, maxFiles: FILE_LIMITS.maxFiles, maxTotalBytes: FILE_LIMITS.maxTotalBytes, publicLinkCapped: false };
|
|
507
|
+
}
|
|
508
|
+
/** A size in MB as the limits are worded ("50", "2.5") — web-upload.ts mbText. */
|
|
509
|
+
function mbText(bytes) { return String(Math.round((bytes / 1024 / 1024) * 10) / 10); }
|
|
510
|
+
/** #1937: the words for a file checkFiles() refused — a chat string key and the numbers it says, from fileLimits(). */
|
|
511
|
+
function fileRejection(reason) {
|
|
512
|
+
var l = fileLimits();
|
|
513
|
+
if (reason === "too-large") return { key: l.publicLinkCapped ? "chat.file_too_large_public" : "chat.file_too_large", values: [mbText(l.maxFileBytes)] };
|
|
514
|
+
if (reason === "too-many") return { key: "chat.file_too_many", values: [l.maxFiles] };
|
|
515
|
+
if (reason === "too-much") return { key: "chat.file_too_much", values: [mbText(l.maxTotalBytes)] };
|
|
516
|
+
return { key: "chat.file_" + String(reason).replace(/-/g, "_"), values: [] };
|
|
517
|
+
}
|
|
494
518
|
|
|
495
519
|
function formatSize(n) {
|
|
496
520
|
var b = Number(n) || 0;
|
|
@@ -504,6 +528,7 @@
|
|
|
504
528
|
* Returns the files kept (pending first, then the accepted new ones) and, for each refused one, why.
|
|
505
529
|
*/
|
|
506
530
|
function checkFiles(pending, adding) {
|
|
531
|
+
var limits = fileLimits();
|
|
507
532
|
var kept = (pending || []).slice();
|
|
508
533
|
var total = 0;
|
|
509
534
|
kept.forEach(function (f) { total += Number(f.size) || 0; });
|
|
@@ -511,9 +536,9 @@
|
|
|
511
536
|
(adding || []).forEach(function (f) {
|
|
512
537
|
var size = Number(f.size) || 0;
|
|
513
538
|
if (size === 0) rejected.push({ name: f.name, reason: "empty" });
|
|
514
|
-
else if (size >
|
|
515
|
-
else if (kept.length >=
|
|
516
|
-
else if (total + size >
|
|
539
|
+
else if (size > limits.maxFileBytes) rejected.push({ name: f.name, reason: "too-large" });
|
|
540
|
+
else if (kept.length >= limits.maxFiles) rejected.push({ name: f.name, reason: "too-many" });
|
|
541
|
+
else if (total + size > limits.maxTotalBytes) rejected.push({ name: f.name, reason: "too-much" });
|
|
517
542
|
else { kept.push(f); total += size; }
|
|
518
543
|
});
|
|
519
544
|
return { kept: kept, rejected: rejected };
|
|
@@ -586,7 +611,7 @@
|
|
|
586
611
|
renderMarkdown: renderMarkdown, escapeHtml: escapeHtml, mergeMessages: mergeMessages, applyReplyButtons: applyReplyButtons, composerKey: composerKey,
|
|
587
612
|
pollRemaining: pollRemaining,
|
|
588
613
|
settleFailedSend: settleFailedSend, putBack: putBack,
|
|
589
|
-
FILE_LIMITS: FILE_LIMITS, formatSize: formatSize, checkFiles: checkFiles, attachmentsHtml: attachmentsHtml, isHtmlAttachment: isHtmlAttachment,
|
|
614
|
+
FILE_LIMITS: FILE_LIMITS, fileLimits: fileLimits, fileRejection: fileRejection, formatSize: formatSize, checkFiles: checkFiles, attachmentsHtml: attachmentsHtml, isHtmlAttachment: isHtmlAttachment,
|
|
590
615
|
nextDeliveryState: nextDeliveryState, applyDelivery: applyDelivery, deliveryHtml: deliveryHtml, isBusy: isBusy,
|
|
591
616
|
isNearBottom: isNearBottom, CODE_FOLD_LINES: CODE_FOLD_LINES, lineCount: lineCount,
|
|
592
617
|
LONG_PASTE_CHARS: LONG_PASTE_CHARS, isLongPaste: isLongPaste, formatElapsed: formatElapsed,
|
package/dist/ui/chat-store.js
CHANGED
|
@@ -269,7 +269,7 @@ export function createChatStore(deps) {
|
|
|
269
269
|
const inFlight = s.inFlightFiles[target] || [];
|
|
270
270
|
const res = R().checkFiles(inFlight.concat(s.pendingFiles[target] || []), [...(list || [])]);
|
|
271
271
|
s.pendingFiles[target] = res.kept.slice(inFlight.length);
|
|
272
|
-
for (const x of res.rejected) deps.toast(`${x.name}: ${t(
|
|
272
|
+
for (const x of res.rejected) { const why = R().fileRejection(x.reason); deps.toast(`${x.name}: ${t(why.key, ...why.values)}`, false); }
|
|
273
273
|
changed(target);
|
|
274
274
|
}
|
|
275
275
|
function removeFile(target, index) { (s.pendingFiles[target] || []).splice(index, 1); changed(target); }
|
|
@@ -353,21 +353,21 @@ export function createChatStore(deps) {
|
|
|
353
353
|
}
|
|
354
354
|
// Board 003648ac: an answer the server marks `sent` leaves by itself; a sent /compact says "compacting…" until the
|
|
355
355
|
// status frame's `compact` (same watch id) says it ended, then leaves when it is done. Everything else — a refusal
|
|
356
|
-
// (even one worded as text), a list to choose from, an answer to read, a compaction timed out or
|
|
356
|
+
// (even one worded as text), a list to choose from, an answer to read, a compaction timed out, lost or not made — stays.
|
|
357
357
|
function showCommand(target, card) {
|
|
358
358
|
s.commands[target] = card;
|
|
359
359
|
const later = (ms, fn) => deps.setTimeout(() => { if (s.commands[target] === card) { fn(); changed(target); } }, ms);
|
|
360
360
|
const leave = () => { delete s.commands[target]; };
|
|
361
|
-
if (card.busy || card.error || card.choices ||
|
|
361
|
+
if (card.busy || card.error || card.choices || ["timeout", "lost", "not_compacted"].includes(card.compact)) return;
|
|
362
362
|
if (card.compact === "running") later(COMPACT_PAGE_LIMIT_MS, () => showCommand(target, { ...card, compact: "timeout" }));
|
|
363
363
|
else if (card.compact === "done") later(COMPACT_DONE_CLEAR_MS, leave);
|
|
364
364
|
else if (card.compact === "unknown") later(COMPACT_UNKNOWN_CLEAR_MS, leave);
|
|
365
365
|
else if (card.sent === true) later(SENT_CLEAR_MS, leave);
|
|
366
366
|
}
|
|
367
|
-
/** The status frame's word on a followed /compact: done, timed out, or lost. */
|
|
367
|
+
/** The status frame's word on a followed /compact: done, not compacted (kiro said so, board 907c7899), timed out, or lost. */
|
|
368
368
|
function noteCompact(name, view) {
|
|
369
369
|
const c = s.commands[name];
|
|
370
|
-
if (!c || c.compact !== "running" || !view || view.id !== c.watch || !["done", "timeout", "lost"].includes(view.state)) return;
|
|
370
|
+
if (!c || c.compact !== "running" || !view || view.id !== c.watch || !["done", "not_compacted", "timeout", "lost"].includes(view.state)) return;
|
|
371
371
|
showCommand(name, { ...c, compact: view.state });
|
|
372
372
|
changed(name);
|
|
373
373
|
}
|
package/dist/ui/chat-strings.js
CHANGED
|
@@ -7,6 +7,7 @@ register("chat", {
|
|
|
7
7
|
compactRunning: "Compacting…", compactDone: "✅ Conversation compacted", compactUnknown: "This backend does not report when compaction ends.",
|
|
8
8
|
compactTimeout: "No sign that compaction finished within {0} minutes — check the agent's own chat.",
|
|
9
9
|
compactLost: "Can't follow this compaction: the command did not reach the agent, or the agent stopped or changed backend.",
|
|
10
|
+
compactNotCompacted: "kiro did not compact the conversation: it was too short, or the compaction was stopped.",
|
|
10
11
|
cmdChoose: "Choose", cmdCurrent: "current", chipCompact: "Compact", chipClear: "Clear…", chipContext: "Context {0}% used",
|
|
11
12
|
chipModel: "Model: {0} — choose another", chipEffort: "Effort: {0} — choose another",
|
|
12
13
|
cmd_ctx: "Context use, model and effort", cmd_compact: "Compact the conversation", cmd_clear: "Clear the conversation (asks first)",
|
|
@@ -69,11 +70,12 @@ register("chat", {
|
|
|
69
70
|
pvFail_unknown: "an unexpected error",
|
|
70
71
|
pvServer_notOffered: "Previews are not available on this page.",
|
|
71
72
|
previews: "Allow HTML previews",
|
|
73
|
+
clicked: "Clicked:", clickShow: "Show the message these buttons were on",
|
|
72
74
|
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",
|
|
73
75
|
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",
|
|
74
76
|
promptAnswering: "Answering…", promptGone: "This prompt is no longer open", promptFor: "{0}: {1}",
|
|
75
77
|
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",
|
|
76
|
-
file_empty: "empty file", file_too_large: "over
|
|
78
|
+
file_empty: "empty file", file_too_large: "over {0} MB", file_too_large_public: "over {0} MB — the most the public link carries in one upload", file_too_many: "at most {0} files per message", file_too_much: "the files together would be over {0} MB", removeFile: "Remove", notSent: "Not sent:", putBack: "Put back", discard: "Discard",
|
|
77
79
|
noMessages: "Type a task below.",
|
|
78
80
|
pick: "Choose an agent to start chatting.", noAgents: "Create an agent to start chatting.", newAgent: "New agent", notFound: "No instance called “{0}”", notFoundHint: "It may have been deleted or renamed.", backToList: "Choose an agent",
|
|
79
81
|
details: "Instance details", start: "Start instance", stopInstance: "Stop instance", restart: "Restart instance", delete: "Delete instance…",
|
|
@@ -95,6 +97,7 @@ register("chat", {
|
|
|
95
97
|
compactRunning: "壓縮中…", compactDone: "✅ 對話已壓縮", compactUnknown: "這個後端不會回報壓縮何時完成。",
|
|
96
98
|
compactTimeout: "{0} 分鐘內沒有看到壓縮完成的紀錄,請到代理自己的對話確認。",
|
|
97
99
|
compactLost: "無法再追蹤這次壓縮:指令沒有送到代理,或代理已停止、換了後端。",
|
|
100
|
+
compactNotCompacted: "kiro 沒有壓縮對話:對話太短,或壓縮被中止了。",
|
|
98
101
|
cmdChoose: "選擇", cmdCurrent: "目前", chipCompact: "壓縮", chipClear: "清空…", chipContext: "已用 {0}% 的 context",
|
|
99
102
|
chipModel: "模型:{0}——選擇其他", chipEffort: "推理強度:{0}——選擇其他",
|
|
100
103
|
cmd_ctx: "context 用量、模型與推理強度", cmd_compact: "壓縮對話", cmd_clear: "清空對話(會先確認)",
|
|
@@ -156,11 +159,12 @@ register("chat", {
|
|
|
156
159
|
pvFail_unknown: "發生了未預期的錯誤",
|
|
157
160
|
pvServer_notOffered: "這個頁面不提供預覽。",
|
|
158
161
|
previews: "允許預覽 HTML",
|
|
162
|
+
clicked: "點選了:", clickShow: "顯示這些按鈕所在的訊息",
|
|
159
163
|
copy: "複製", copyMessage: "複製這則回覆", copied: "已複製", copyFailed: "複製失敗", wrap: "換行", showAll: "展開全部 {0} 行", showMore: "顯示更多", showLess: "收合", jumpLatest: "最新", newBelow: "{0} 則新訊息",
|
|
160
164
|
disconnected: "已斷線", tick_queued: "排在前一則訊息之後", tick_processing: "已交給 agent", tick_delivered: "agent 已收到", tick_failed: "未送達", tick_cancelled: "已被中止取消",
|
|
161
165
|
promptAnswering: "回覆中…", promptGone: "這個提示已經結束", promptFor: "{0}:{1}",
|
|
162
166
|
working: "{0} 處理中…", stuck: "{0} 似乎卡住了", stopReply: "中止回覆", stopReplyTitle: "中止目前的回覆(同 Esc),並取消還在排隊的訊息。instance 會繼續運行。", stopSent: "已中止",
|
|
163
|
-
file_empty: "空檔案", file_too_large: "超過
|
|
167
|
+
file_empty: "空檔案", file_too_large: "超過 {0} MB", file_too_large_public: "超過 {0} MB,這是公開連結單次上傳的上限", file_too_many: "每則訊息最多 {0} 個檔案", file_too_much: "檔案合計會超過 {0} MB", removeFile: "移除", notSent: "未送出:", putBack: "放回輸入框", discard: "捨棄",
|
|
164
168
|
noMessages: "直接在下方輸入任務。",
|
|
165
169
|
pick: "選一個 Agent 開始聊天。", noAgents: "先建立一個 Agent,再開始聊天。", newAgent: "新增 Agent", notFound: "沒有叫「{0}」的 instance", notFoundHint: "它可能已被刪除或改名。", backToList: "選擇 Agent",
|
|
166
170
|
details: "Instance 詳細資料", start: "啟動 instance", stopInstance: "停止 instance", restart: "重新啟動 instance", delete: "刪除 instance…",
|
package/dist/ui/chat-thread.js
CHANGED
|
@@ -31,6 +31,12 @@ const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill=
|
|
|
31
31
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
32
32
|
export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); }
|
|
33
33
|
export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
34
|
+
/**
|
|
35
|
+
* Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
|
|
36
|
+
* at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
|
|
37
|
+
*/
|
|
38
|
+
export const THREAD_TAIL = 24;
|
|
39
|
+
export const THREAD_CHUNK = 40;
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
@@ -47,6 +53,10 @@ export function createThread(list, scroller, opts) {
|
|
|
47
53
|
// moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
|
|
48
54
|
let pinned = true;
|
|
49
55
|
let lastTop = 0;
|
|
56
|
+
// The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
|
|
57
|
+
// drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
|
|
58
|
+
let firstKey = null;
|
|
59
|
+
let growTimer = null, growFrame = null, disposed = false;
|
|
50
60
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
51
61
|
const tr = opts.t, trf = opts.tf;
|
|
52
62
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
@@ -76,7 +86,20 @@ export function createThread(list, scroller, opts) {
|
|
|
76
86
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
77
87
|
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>`;
|
|
78
88
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
79
|
-
|
|
89
|
+
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
90
|
+
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
91
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<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>`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
95
|
+
* reply is in this thread) it is a button that shows that reply; restored from disk, plain.
|
|
96
|
+
*/
|
|
97
|
+
function clickHtml(click) {
|
|
98
|
+
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
99
|
+
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
100
|
+
return `<div class="click-chip">${of
|
|
101
|
+
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
102
|
+
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
80
103
|
}
|
|
81
104
|
/**
|
|
82
105
|
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
@@ -428,6 +451,44 @@ export function createThread(list, scroller, opts) {
|
|
|
428
451
|
const ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
429
452
|
if (ro) { ro.observe(list); ro.observe(scroller); }
|
|
430
453
|
|
|
454
|
+
/** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
|
|
455
|
+
function startIndex() {
|
|
456
|
+
const i = firstKey === null ? -1 : msgs.findIndex((x) => msgKey(x) === firstKey);
|
|
457
|
+
// Whatever is drawn stays drawn: the list may have reordered (merged across fleet restarts) or lost its oldest to
|
|
458
|
+
// the cap — so the window starts at the earlier of the oldest drawn key and the first message still drawn.
|
|
459
|
+
const still = nodes.size ? msgs.findIndex((x) => nodes.has(msgKey(x))) : -1;
|
|
460
|
+
const at = [i, still].filter((n) => n >= 0);
|
|
461
|
+
return at.length ? Math.min(...at) : Math.max(0, msgs.length - THREAD_TAIL);
|
|
462
|
+
}
|
|
463
|
+
/** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
|
|
464
|
+
function grow() {
|
|
465
|
+
growTimer = growFrame = null;
|
|
466
|
+
if (disposed) return; // the chat left: nothing more is drawn
|
|
467
|
+
const at = startIndex();
|
|
468
|
+
if (at <= 0) return;
|
|
469
|
+
firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
|
|
470
|
+
const fromBottom = scroller.scrollHeight - scroller.scrollTop;
|
|
471
|
+
draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
|
|
472
|
+
if (pinned) toBottom();
|
|
473
|
+
else { scroller.scrollTop = scroller.scrollHeight - fromBottom; lastTop = scroller.scrollTop; }
|
|
474
|
+
scheduleGrow();
|
|
475
|
+
}
|
|
476
|
+
/** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
|
|
477
|
+
function scheduleGrow() {
|
|
478
|
+
if (growTimer !== null || growFrame !== null || startIndex() <= 0) return;
|
|
479
|
+
const later = () => { growFrame = null; growTimer = setTimeout(grow, 0); };
|
|
480
|
+
if (typeof requestAnimationFrame === "function") growFrame = requestAnimationFrame(later); else later();
|
|
481
|
+
}
|
|
482
|
+
/** Everything drawn now (a remembered scroll position, a message to show that is not drawn yet). */
|
|
483
|
+
function growAll() {
|
|
484
|
+
if (startIndex() <= 0) return;
|
|
485
|
+
if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
|
|
486
|
+
if (growTimer !== null) clearTimeout(growTimer);
|
|
487
|
+
growTimer = growFrame = null;
|
|
488
|
+
firstKey = msgs.length ? msgKey(msgs[0]) : null;
|
|
489
|
+
draw({ older: true });
|
|
490
|
+
}
|
|
491
|
+
|
|
431
492
|
/** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
|
|
432
493
|
function render(next, renderOpts = {}) {
|
|
433
494
|
const stick = pinned || nearBottom();
|
|
@@ -435,24 +496,43 @@ export function createThread(list, scroller, opts) {
|
|
|
435
496
|
if (!msgs.length) {
|
|
436
497
|
for (const v of nodes.values()) stopIn(v.node);
|
|
437
498
|
nodes.clear();
|
|
499
|
+
firstKey = null;
|
|
438
500
|
armDay();
|
|
439
501
|
list.textContent = "";
|
|
440
502
|
opts.onEmpty(true);
|
|
441
503
|
return;
|
|
442
504
|
}
|
|
443
505
|
opts.onEmpty(false);
|
|
444
|
-
|
|
506
|
+
// A position to return to that is not the bottom needs the messages above it: all of them, at once (as before).
|
|
507
|
+
if ("restore" in renderOpts && renderOpts.restore != null) firstKey = msgKey(msgs[0]);
|
|
508
|
+
const added = draw({});
|
|
509
|
+
if (firstKey === null || startIndex() > 0) { firstKey = msgKey(msgs[startIndex()]); scheduleGrow(); }
|
|
510
|
+
if ("restore" in renderOpts) {
|
|
511
|
+
unseen = 0;
|
|
512
|
+
if (renderOpts.restore == null) toBottom();
|
|
513
|
+
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
514
|
+
} else if (stick) toBottom();
|
|
515
|
+
else unseen += added;
|
|
516
|
+
reportJump();
|
|
517
|
+
}
|
|
518
|
+
/** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
|
|
519
|
+
function draw(drawOpts) {
|
|
520
|
+
const shown = msgs.slice(startIndex());
|
|
521
|
+
const keep = new Set(shown.map(msgKey));
|
|
445
522
|
for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
|
|
446
523
|
let prev = null, added = 0;
|
|
447
|
-
for (const x of
|
|
448
|
-
const key = msgKey(x)
|
|
524
|
+
for (const x of shown) {
|
|
525
|
+
const key = msgKey(x);
|
|
449
526
|
let have = nodes.get(key);
|
|
527
|
+
// Filling in older messages: a node drawn from this very message object is current — not drawn again.
|
|
528
|
+
if (drawOpts.older && have && have.x === x) { prev = have.node; continue; }
|
|
529
|
+
const html = msgHtml(x);
|
|
450
530
|
if (!have || have.html !== html) {
|
|
451
531
|
const stopped = have ? stopIn(have.node) : [];
|
|
452
532
|
const node = msgNode(html, x, stopped);
|
|
453
533
|
if (have) have.node.replaceWith(node); else added++;
|
|
454
|
-
have = { html, node }; nodes.set(key, have);
|
|
455
|
-
} else timeLabel(have.node);
|
|
534
|
+
have = { html, node, x }; nodes.set(key, have);
|
|
535
|
+
} else { have.x = x; timeLabel(have.node); }
|
|
456
536
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
457
537
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
458
538
|
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
@@ -460,13 +540,7 @@ export function createThread(list, scroller, opts) {
|
|
|
460
540
|
}
|
|
461
541
|
armDay();
|
|
462
542
|
armPolls();
|
|
463
|
-
|
|
464
|
-
unseen = 0;
|
|
465
|
-
if (renderOpts.restore == null) toBottom();
|
|
466
|
-
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
467
|
-
} else if (stick) toBottom();
|
|
468
|
-
else unseen += added;
|
|
469
|
-
reportJump();
|
|
543
|
+
return added;
|
|
470
544
|
}
|
|
471
545
|
|
|
472
546
|
function onScroll() {
|
|
@@ -481,6 +555,7 @@ export function createThread(list, scroller, opts) {
|
|
|
481
555
|
function jumpLatest() { toBottom(); unseen = 0; reportJump(); }
|
|
482
556
|
/** Show one message (the panel's "from …" link, #1481): scrolled into view and briefly marked. */
|
|
483
557
|
function reveal(key) {
|
|
558
|
+
if (!nodes.has(key) && msgs.some((x) => msgKey(x) === key)) growAll(); // an older message not drawn yet
|
|
484
559
|
const have = nodes.get(key); if (!have) return false;
|
|
485
560
|
if (typeof have.node.scrollIntoView === "function") have.node.scrollIntoView({ block: "center" });
|
|
486
561
|
have.node.classList.add("flash");
|
|
@@ -530,11 +605,12 @@ export function createThread(list, scroller, opts) {
|
|
|
530
605
|
else if (act === "replyButton") {
|
|
531
606
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
532
607
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
533
|
-
}
|
|
608
|
+
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
534
609
|
}
|
|
535
610
|
|
|
536
611
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
537
612
|
function dispose() {
|
|
613
|
+
disposed = true;
|
|
538
614
|
if (ro) ro.disconnect();
|
|
539
615
|
if (P()) P().stopAll("leave");
|
|
540
616
|
nodes.clear();
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -324,7 +324,7 @@ function CommandCard({ name, lease }) {
|
|
|
324
324
|
${c.text ? html`<pre class="cmd-text">${c.text}</pre>` : null}
|
|
325
325
|
${c.compact ? html`<p class="cmd-text cmd-phase">${c.compact === "running" ? t("chat.compactRunning")
|
|
326
326
|
: c.compact === "done" ? t("chat.compactDone") : c.compact === "timeout" ? t("chat.compactTimeout", "10")
|
|
327
|
-
: c.compact === "lost" ? t("chat.compactLost") : t("chat.compactUnknown")}</p>` : null}
|
|
327
|
+
: c.compact === "lost" ? t("chat.compactLost") : c.compact === "not_compacted" ? t("chat.compactNotCompacted") : t("chat.compactUnknown")}</p>` : null}
|
|
328
328
|
${c.choices && c.choices.prompt ? html`<pre class="cmd-text">${c.choices.prompt}</pre>` : null}
|
|
329
329
|
${c.choices ? html`<div class="cmd-choices" role="group" aria-label=${t("chat.cmdChoose")}>${c.choices.options.map((o) => html`<button key=${o.id} type="button"
|
|
330
330
|
class=${`btn btn-sm${o.id === c.choices.current ? " current" : ""}`} aria-current=${o.id === c.choices.current ? "true" : undefined} onClick=${() => run(o.id)}>${o.label}</button>`)}</div>` : null}
|
|
@@ -761,7 +761,7 @@ function General({ ctx }) {
|
|
|
761
761
|
*/
|
|
762
762
|
export const WEB_READ_ONLY = [
|
|
763
763
|
"web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin", "web.preview_on_public_link",
|
|
764
|
-
"web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
|
|
764
|
+
"web.usage_panel", "web.provider_secrets", "web.upload_max_file_mb", "web.upload_max_total_mb", "web_terminal.bind", "web_terminal.tunnel",
|
|
765
765
|
];
|
|
766
766
|
function WebSection({ ctx, staged }) {
|
|
767
767
|
const { schema } = ctx;
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -382,6 +382,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
382
382
|
such as 「昨天 下午03:58」, ticks) — and a block body would stretch to it; fit-content keeps a short message short, on
|
|
383
383
|
the right under its meta, and a long one still wraps at the message's max-width. */
|
|
384
384
|
.msg.user .body { background: var(--user-bubble); border-radius: var(--r-xl); padding: 10px var(--s-4); width: fit-content; max-width: 100%; margin-left: auto; }
|
|
385
|
+
/* A person's click on a reply button: the bubble shows 🔘 and the button's label (never the value the agent was given);
|
|
386
|
+
live, the label is a button that shows the reply its buttons were on. */
|
|
387
|
+
.click-chip { display: flex; }
|
|
388
|
+
.click-chip .click-link, .click-chip .click-text { display: inline-flex; align-items: baseline; gap: var(--s-1); padding: 0; border: 0; background: none; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; }
|
|
389
|
+
.click-chip .click-link { cursor: pointer; }
|
|
390
|
+
.click-chip .click-link:hover, .click-chip .click-link:focus-visible { text-decoration: underline; }
|
|
391
|
+
.click-mark { flex: none; }
|
|
385
392
|
.msg.agent { align-self: stretch; }
|
|
386
393
|
/* #1672: a status line (another instance's message, a daemon notice): muted plain text, no bubble; a long one folds.
|
|
387
394
|
Its class is `status-line`, never `status`: that is the header's inline-flex badge class, and on a message it turned
|
package/dist/web-api.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface WebApiContext {
|
|
|
39
39
|
web?: {
|
|
40
40
|
echo_to_channel?: boolean;
|
|
41
41
|
usage_panel?: boolean;
|
|
42
|
+
upload_max_file_mb?: number;
|
|
43
|
+
upload_max_total_mb?: number;
|
|
42
44
|
};
|
|
43
45
|
defaults?: {
|
|
44
46
|
backend?: string;
|
|
@@ -294,6 +296,8 @@ export interface AppShellContext {
|
|
|
294
296
|
web?: {
|
|
295
297
|
usage_panel?: boolean;
|
|
296
298
|
log_tab?: boolean;
|
|
299
|
+
upload_max_file_mb?: number;
|
|
300
|
+
upload_max_total_mb?: number;
|
|
297
301
|
};
|
|
298
302
|
} | null;
|
|
299
303
|
/** #1665: the host's theme and language go into the page itself, so a device paints them before any script reads. */
|