@songsid/agend 2.2.0-beta.17 → 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.
Files changed (50) hide show
  1. package/README.md +18 -18
  2. package/README.zh-TW.md +18 -18
  3. package/dist/agent-whats-new.js +3 -1
  4. package/dist/agent-whats-new.js.map +1 -1
  5. package/dist/backend/kiro.d.ts +2 -0
  6. package/dist/backend/kiro.js +4 -0
  7. package/dist/backend/kiro.js.map +1 -1
  8. package/dist/backend/types.d.ts +5 -0
  9. package/dist/backend/types.js.map +1 -1
  10. package/dist/compact-watch.d.ts +9 -1
  11. package/dist/compact-watch.js +47 -6
  12. package/dist/compact-watch.js.map +1 -1
  13. package/dist/config-validator.js +26 -1
  14. package/dist/config-validator.js.map +1 -1
  15. package/dist/daemon.d.ts +8 -0
  16. package/dist/daemon.js +65 -5
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/fleet-manager.js +49 -20
  19. package/dist/fleet-manager.js.map +1 -1
  20. package/dist/forged-envelope-scan.d.ts +4 -3
  21. package/dist/forged-envelope-scan.js +4 -3
  22. package/dist/forged-envelope-scan.js.map +1 -1
  23. package/dist/kiro-compact-pane.d.ts +38 -0
  24. package/dist/kiro-compact-pane.js +57 -0
  25. package/dist/kiro-compact-pane.js.map +1 -0
  26. package/dist/locale.js +4 -4
  27. package/dist/locale.js.map +1 -1
  28. package/dist/outbound-schemas.js +1 -1
  29. package/dist/outbound-schemas.js.map +1 -1
  30. package/dist/public-web-gateway.d.ts +2 -0
  31. package/dist/public-web-gateway.js +6 -1
  32. package/dist/public-web-gateway.js.map +1 -1
  33. package/dist/types.d.ts +8 -0
  34. package/dist/ui/chat-render.js +31 -6
  35. package/dist/ui/chat-store.js +5 -5
  36. package/dist/ui/chat-strings.js +6 -2
  37. package/dist/ui/chat-thread.js +15 -2
  38. package/dist/ui/panel-chat.js +1 -1
  39. package/dist/ui/panel-settings.js +1 -1
  40. package/dist/ui/shared/app.css +7 -0
  41. package/dist/web-api.d.ts +4 -0
  42. package/dist/web-api.js +135 -43
  43. package/dist/web-api.js.map +1 -1
  44. package/dist/web-chat-history.d.ts +14 -0
  45. package/dist/web-chat-history.js +18 -1
  46. package/dist/web-chat-history.js.map +1 -1
  47. package/dist/web-upload.d.ts +112 -1
  48. package/dist/web-upload.js +237 -36
  49. package/dist/web-upload.js.map +1 -1
  50. package/package.json +3 -3
@@ -4,6 +4,9 @@ import { applyWebSecurityHeaders } from "./web-host-guard.js";
4
4
  import { isServedAsset } from "./auth-api.js";
5
5
  import { shellRoute } from "./web-shell-routes.js";
6
6
  import { isWebIconPath } from "./web-icons.js";
7
+ import { UPLOAD_REQUEST_TIMEOUT_MS } from "./web-upload.js";
8
+ /** #1937: how long a public-link request other than an upload may take to arrive whole (the gateway's old requestTimeout). */
9
+ export const PUBLIC_GATEWAY_RECEIVE_MS = 10_000;
7
10
  /** Reviewed panel routes. No health, agent, issue-code, preview, legacy restart or SSE. */
8
11
  export function isPublicWebRoute(method, path) {
9
12
  const reads = method === "GET" || method === "HEAD";
@@ -112,7 +115,9 @@ export function createPublicWebGateway(opts) {
112
115
  });
113
116
  server.maxHeadersCount = 64;
114
117
  server.headersTimeout = 5_000;
115
- server.requestTimeout = 10_000;
118
+ // #1937: an upload may take long on a slow link (its own clocks bound it); every other request is still dropped if it
119
+ // has not arrived whole in PUBLIC_GATEWAY_RECEIVE_MS (limitRequestReceive, where the request is dispatched).
120
+ server.requestTimeout = UPLOAD_REQUEST_TIMEOUT_MS;
116
121
  server.keepAliveTimeout = 5_000;
117
122
  server.setTimeout(15_000, socket => socket.destroy());
118
123
  server.on("connection", socket => { if (sockets.size >= 32) {
@@ -1 +1 @@
1
- {"version":3,"file":"public-web-gateway.js","sourceRoot":"","sources":["../src/public-web-gateway.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"public-web-gateway.js","sourceRoot":"","sources":["../src/public-web-gateway.ts"],"names":[],"mappings":"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"}
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
@@ -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); checked here first only to say so early. */
493
- var FILE_LIMITS = { maxFileBytes: 10 * 1024 * 1024, maxFiles: 5, maxTotalBytes: 25 * 1024 * 1024 };
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 > FILE_LIMITS.maxFileBytes) rejected.push({ name: f.name, reason: "too-large" });
515
- else if (kept.length >= FILE_LIMITS.maxFiles) rejected.push({ name: f.name, reason: "too-many" });
516
- else if (total + size > FILE_LIMITS.maxTotalBytes) rejected.push({ name: f.name, reason: "too-much" });
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,
@@ -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(`chat.file_${x.reason.replace(/-/g, "_")}`)}`, false);
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 lost — stays.
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 || card.compact === "timeout" || card.compact === "lost") return;
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
  }
@@ -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 10 MB", file_too_many: "at most 5 files per message", file_too_much: "the files together would be over 25 MB", removeFile: "Remove", notSent: "Not sent:", putBack: "Put back", discard: "Discard",
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: "超過 10 MB", file_too_many: "每則訊息最多 5 個檔案", file_too_much: "檔案合計會超過 25 MB", removeFile: "移除", notSent: "未送出:", putBack: "放回輸入框", discard: "捨棄",
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…",
@@ -86,7 +86,20 @@ export function createThread(list, scroller, opts) {
86
86
  const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
87
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>`;
88
88
  const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
89
- return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${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>`;
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>`;
90
103
  }
91
104
  /**
92
105
  * #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
@@ -592,7 +605,7 @@ export function createThread(list, scroller, opts) {
592
605
  else if (act === "replyButton") {
593
606
  const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
594
607
  if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
595
- }
608
+ } else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
596
609
  }
597
610
 
598
611
  /** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
@@ -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;
@@ -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. */
package/dist/web-api.js CHANGED
@@ -15,7 +15,7 @@ import { isSafeInstanceName } from "./web-shell-routes.js";
15
15
  import { MAX_COMMAND_ARGS } from "./web-commands.js";
16
16
  import { sendPanelHtml } from "./web-host-guard.js";
17
17
  import { shellRoute } from "./web-shell-routes.js";
18
- import { readFileSync } from "node:fs";
18
+ import { createReadStream, createWriteStream, readFileSync } from "node:fs";
19
19
  import { join, dirname } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { CreateInstanceArgs, CreateScheduleArgs, UpdateScheduleArgs, validateArgs } from "./outbound-schemas.js";
@@ -26,7 +26,7 @@ import { z } from "zod";
26
26
  import { evaluateWebRequest, isPassiveWebRead, isSecureRequest, isWebRequestAuthorized, WEB_TOKEN_INVALID_MESSAGE } from "./web-auth.js";
27
27
  import { dashboardOriginFor } from "./web-preview.js";
28
28
  import { newWebMessageId, parseLastEventId } from "./web-chat-history.js";
29
- import { attachmentDelivery, displayName, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, sniffUpload, UPLOAD_LIMITS, wellFormed } from "./web-upload.js";
29
+ import { attachmentDelivery, createPartFile, displayName, fileTooLargeText, INLINE_MIME, isFileId, MEDIA_RANGE_MAX, publicAttachment, UPLOAD_LIMITS, UPLOAD_MAX_MS, UPLOAD_STALL_MS, UploadSniffer, uploadLimits, wellFormed } from "./web-upload.js";
30
30
  import { getAgendHome } from "./paths.js";
31
31
  import { OPERATOR } from "./web-prefs.js";
32
32
  import { WEB_NAME_FALLBACK } from "./web-profile.js";
@@ -442,12 +442,14 @@ export function handleWebRequest(req, res, url, ctx) {
442
442
  serveMediaRange(req, res, ctx.webFiles, id, media);
443
443
  return true;
444
444
  }
445
- const got = isFileId(id) ? ctx.webFiles?.read(id) : null;
446
- if (!got) {
445
+ // #1937: a file over MEDIA_RANGE_MAX streams from the descriptor its checks were made on — never whole in memory.
446
+ const big = isFileId(id) && (ctx.webFiles?.servedFile?.(id)?.size ?? 0) > MEDIA_RANGE_MAX ? ctx.webFiles?.open?.(id) ?? null : null;
447
+ const got = big ? null : isFileId(id) ? ctx.webFiles?.read(id) : null;
448
+ const file = big?.file ?? got?.file;
449
+ if (!file) {
447
450
  json(res, 404, { error: "No such file" });
448
451
  return true;
449
452
  }
450
- const { file, bytes } = got;
451
453
  const inline = INLINE_MIME.has(file.mime);
452
454
  // Only the four image types are shown in the page; anything else is a download, never rendered.
453
455
  res.setHeader("Content-Type", inline ? file.mime : (file.mime.startsWith("text/") ? "text/plain; charset=utf-8" : "application/octet-stream"));
@@ -455,8 +457,12 @@ export function handleWebRequest(req, res, url, ctx) {
455
457
  res.setHeader("X-Content-Type-Options", "nosniff");
456
458
  res.setHeader("Cache-Control", "private, no-store");
457
459
  res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
460
+ if (big) {
461
+ streamWhole(res, big);
462
+ return true;
463
+ }
458
464
  res.writeHead(200);
459
- res.end(bytes);
465
+ res.end(got.bytes);
460
466
  return true;
461
467
  }
462
468
  // ── #1630: avatars and Discord server emoji, same-origin (the page never loads a platform's CDN; img-src stays 'self') ──
@@ -1622,6 +1628,11 @@ function handleSendMessage(req, res, ctx) {
1622
1628
  * One file for one instance's chat. The body is the file itself (Content-Type is ignored: the type is
1623
1629
  * read from the bytes), `X-Agend-Filename` its name for the label only. Stored in the instance's
1624
1630
  * workspace inbox under a name chosen here; answered with the id the message will name.
1631
+ *
1632
+ * #1937: streamed to a part file in that inbox (web-pending-….part) as it arrives — never held whole in memory — and
1633
+ * counted by the bytes that came, whatever Content-Length said. The limits are the ones in force for this request
1634
+ * (uploadLimits(): the config, and the public link's own cap). An upload that goes UPLOAD_STALL_MS without a byte, or
1635
+ * runs past UPLOAD_MAX_MS, is dropped; any upload that does not end in a stored file takes its part file with it.
1625
1636
  */
1626
1637
  function handleUpload(req, res, url, ctx) {
1627
1638
  const instance = url.searchParams.get("instance") ?? "";
@@ -1633,57 +1644,112 @@ function handleUpload(req, res, url, ctx) {
1633
1644
  json(res, 404, { error: `Instance not found: ${instance}` });
1634
1645
  return;
1635
1646
  }
1647
+ const limits = uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined });
1648
+ const tooLarge = () => json(res, 413, { error: fileTooLargeText(limits), max_bytes: limits.maxFileBytes, ...(limits.publicLinkCapped ? { public_link: true } : {}) });
1636
1649
  const declared = Number(req.headers["content-length"] ?? NaN);
1637
- if (Number.isFinite(declared) && declared > UPLOAD_LIMITS.maxFileBytes) {
1638
- json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
1650
+ if (Number.isFinite(declared) && declared > limits.maxFileBytes) {
1651
+ tooLarge();
1652
+ req.resume();
1653
+ return;
1654
+ }
1655
+ let part, fd, discard;
1656
+ try {
1657
+ ({ path: part, fd, discard } = createPartFile(join(getAgendHome(), "workspaces", instance, "inbox")));
1658
+ }
1659
+ catch (err) {
1660
+ ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
1661
+ json(res, 500, { error: "the file could not be stored" });
1639
1662
  req.resume();
1640
1663
  return;
1641
1664
  }
1642
- const chunks = [];
1665
+ const out = createWriteStream(part, { fd, autoClose: true });
1666
+ const sniffer = new UploadSniffer();
1643
1667
  let size = 0;
1644
- let refused = false;
1668
+ let over = false; // ended without a stored file: no further write, and the part file goes
1669
+ let stall;
1670
+ const clocks = () => { clearTimeout(stall); clearTimeout(deadline); };
1671
+ /** Stop here: answer (when the client is still there), remove the part file, and stop a body still arriving. */
1672
+ const drop = (answer) => {
1673
+ if (over)
1674
+ return;
1675
+ over = true;
1676
+ clocks();
1677
+ answer?.();
1678
+ out.destroy();
1679
+ discard();
1680
+ if (!req.complete)
1681
+ req.destroy();
1682
+ };
1683
+ const armStall = () => { clearTimeout(stall); stall = setTimeout(() => drop(() => json(res, 408, { error: "the upload stalled — attach the file again" })), UPLOAD_STALL_MS); stall.unref?.(); };
1684
+ const deadline = setTimeout(() => drop(() => json(res, 408, { error: "the upload took too long — attach the file again" })), UPLOAD_MAX_MS);
1685
+ deadline.unref?.();
1686
+ armStall();
1687
+ out.on("error", (err) => { ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored"); drop(() => json(res, 500, { error: "the file could not be stored" })); });
1688
+ out.on("drain", () => { if (!over)
1689
+ req.resume?.(); });
1645
1690
  req.on("data", (c) => {
1646
- if (refused)
1691
+ if (over)
1647
1692
  return;
1648
1693
  size += c.length;
1649
- if (size > UPLOAD_LIMITS.maxFileBytes) {
1650
- refused = true;
1651
- json(res, 413, { error: `a file can be at most ${UPLOAD_LIMITS.maxFileBytes / 1024 / 1024} MB` });
1652
- req.destroy();
1694
+ if (size > limits.maxFileBytes) {
1695
+ drop(tooLarge);
1653
1696
  return;
1654
1697
  }
1655
- chunks.push(c);
1698
+ armStall();
1699
+ sniffer.push(c);
1700
+ if (!out.write(c))
1701
+ req.pause?.();
1656
1702
  });
1657
- req.on("error", () => { });
1703
+ req.on("error", () => drop()); // the client went away
1704
+ req.on("close", () => { if (!over && req.complete === false)
1705
+ drop(); });
1706
+ // The instance is checked again now the whole body is here: it may have been deleted (and its files
1707
+ // forgotten) or stopped while the upload streamed in; storing now would bring its id back (#1252 review).
1708
+ // Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
1709
+ // in fleet.yaml, or as a ClassicBot room in classicBot.yaml. Asked when the body ends and again just before storing.
1710
+ const deliverable = () => ctx.instanceIpcClients.has(instance) && (!!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true);
1711
+ const gone = () => drop(() => json(res, 404, { error: `Instance not found: ${instance}` }));
1658
1712
  req.on("end", () => {
1659
- if (!permitWebContinuation(req, res, ctx))
1713
+ if (over)
1660
1714
  return;
1661
- if (refused)
1715
+ clocks();
1716
+ if (!permitWebContinuation(req, res, ctx)) {
1717
+ drop();
1662
1718
  return;
1663
- const bytes = Buffer.concat(chunks);
1719
+ }
1664
1720
  const name = displayName(typeof req.headers["x-agend-filename"] === "string" ? req.headers["x-agend-filename"] : "", "file");
1665
- const type = sniffUpload(bytes, name);
1721
+ const type = sniffer.result(name);
1666
1722
  if (!type) {
1667
- json(res, 415, { error: "the file is empty" });
1723
+ drop(() => json(res, 415, { error: "the file is empty" }));
1668
1724
  return;
1669
1725
  }
1670
- // The instance is checked again now the whole body is here: it may have been deleted (and its files
1671
- // forgotten) or stopped while the upload streamed in; storing now would bring its id back (#1252 review).
1672
- // Still deliverable means: its IPC is up (a paused one is woken by the send) and it is still registered —
1673
- // in fleet.yaml, or as a ClassicBot room in classicBot.yaml.
1674
- const registered = !!ctx.fleetConfig?.instances?.[instance] || ctx.isClassicInstance?.(instance) === true;
1675
- if (!ctx.instanceIpcClients.has(instance) || !registered) {
1676
- json(res, 404, { error: `Instance not found: ${instance}` });
1726
+ if (!deliverable()) {
1727
+ gone();
1677
1728
  return;
1678
1729
  }
1679
- try {
1680
- const entry = ctx.webFiles.storeUpload({ instance, inboxDir: join(getAgendHome(), "workspaces", instance, "inbox"), bytes, name, type });
1681
- json(res, 200, publicAttachment(entry));
1682
- }
1683
- catch (err) {
1684
- ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
1685
- json(res, 500, { error: "the file could not be stored" });
1686
- }
1730
+ out.end(() => {
1731
+ if (over)
1732
+ return;
1733
+ // The write's finish is another wait (Prism #1941 P2-1): the session or the public link may have ended in it.
1734
+ if (!permitWebContinuation(req, res, ctx)) {
1735
+ drop();
1736
+ return;
1737
+ }
1738
+ if (!deliverable()) {
1739
+ gone();
1740
+ return;
1741
+ }
1742
+ over = true;
1743
+ try {
1744
+ const entry = ctx.webFiles.storeUploadedFile({ instance, partPath: part, name, type });
1745
+ json(res, 200, publicAttachment(entry));
1746
+ }
1747
+ catch (err) {
1748
+ discard();
1749
+ ctx.logger.error({ err: err.message, instance }, "Web upload could not be stored");
1750
+ json(res, 500, { error: "the file could not be stored" });
1751
+ }
1752
+ });
1687
1753
  });
1688
1754
  }
1689
1755
  function shellPreview(req, ctx, mode) {
@@ -1701,6 +1767,15 @@ function shellPreview(req, ctx, mode) {
1701
1767
  * No Range, several, or one not in that form → 200 with the whole file, as every file is served (the Download link).
1702
1768
  * Inline, nosniff, no caching, and the route's own sandbox policy, as for every file.
1703
1769
  */
1770
+ /** #1937: a whole file (headers already set) streamed from the descriptor its checks were made on: 200, its length. */
1771
+ function streamWhole(res, opened) {
1772
+ res.setHeader("Content-Length", String(opened.file.size));
1773
+ res.writeHead(200);
1774
+ const stream = createReadStream("", { fd: opened.fd, start: 0, end: Math.max(0, opened.file.size - 1), autoClose: true });
1775
+ stream.on("error", () => res.destroy());
1776
+ res.on("close", () => stream.destroy());
1777
+ stream.pipe(res);
1778
+ }
1704
1779
  function serveMediaRange(req, res, files, id, file) {
1705
1780
  const total = file.size;
1706
1781
  const header = typeof req.headers.range === "string" ? req.headers.range.trim() : "";
@@ -1724,20 +1799,34 @@ function serveMediaRange(req, res, files, id, file) {
1724
1799
  }
1725
1800
  }
1726
1801
  end = ranged ? Math.min(end, total - 1, start + MEDIA_RANGE_MAX - 1) : total - 1;
1802
+ const mediaHeaders = () => {
1803
+ res.setHeader("Content-Type", file.mime);
1804
+ res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
1805
+ res.setHeader("Accept-Ranges", "bytes");
1806
+ res.setHeader("X-Content-Type-Options", "nosniff");
1807
+ res.setHeader("Cache-Control", "private, no-store");
1808
+ res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
1809
+ };
1810
+ // #1937 (Prism #1941 P3-2): the whole of a file over MEDIA_RANGE_MAX (no Range asked) streams, as /ui/file's do.
1811
+ if (!ranged && total > MEDIA_RANGE_MAX) {
1812
+ const opened = files.open(id);
1813
+ if (!opened) {
1814
+ json(res, 404, { error: "No such file" });
1815
+ return;
1816
+ }
1817
+ mediaHeaders();
1818
+ streamWhole(res, opened);
1819
+ return;
1820
+ }
1727
1821
  const got = files.readSlice(id, start, end);
1728
1822
  if (!got || (!ranged && got.bytes.length !== total)) {
1729
1823
  json(res, 404, { error: "No such file" });
1730
1824
  return;
1731
1825
  }
1732
- res.setHeader("Content-Type", file.mime);
1733
- res.setHeader("Content-Disposition", `inline; filename*=UTF-8''${encodeURIComponent(wellFormed(file.name))}`);
1734
- res.setHeader("Accept-Ranges", "bytes");
1826
+ mediaHeaders();
1735
1827
  if (ranged)
1736
1828
  res.setHeader("Content-Range", `bytes ${got.start}-${got.end}/${total}`);
1737
1829
  res.setHeader("Content-Length", String(got.bytes.length));
1738
- res.setHeader("X-Content-Type-Options", "nosniff");
1739
- res.setHeader("Cache-Control", "private, no-store");
1740
- res.setHeader("Content-Security-Policy", "default-src 'none'; img-src 'self'; style-src 'unsafe-inline'; sandbox");
1741
1830
  res.writeHead(ranged ? 206 : 200);
1742
1831
  res.end(got.bytes);
1743
1832
  }
@@ -1769,6 +1858,9 @@ function shellBodyTag(req, ctx, mode) {
1769
1858
  + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
1770
1859
  // #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
1771
1860
  + ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
1861
+ // #1937: the upload limits in force for this page (the config's; the public link's own cap on it) — what the chat
1862
+ // checks before sending a file, and the numbers its words say. The server checks again on every upload.
1863
+ + ` data-upload-limits="${attr(JSON.stringify(uploadLimits(ctx.fleetConfig?.web, { publicLink: gatewayRequestContext(req) !== undefined })))}"`
1772
1864
  // #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
1773
1865
  + ` data-fleet-tz="${attr(resolveTz())}"`
1774
1866
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.