@songsid/agend 2.2.0-beta.6 → 2.2.0-beta.8

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 (134) hide show
  1. package/dist/agent-cli-instructions.md +6 -2
  2. package/dist/agent-cli.js +9 -5
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-whats-new.js +9 -3
  5. package/dist/agent-whats-new.js.map +1 -1
  6. package/dist/backend/kiro.d.ts +9 -0
  7. package/dist/backend/kiro.js +23 -0
  8. package/dist/backend/kiro.js.map +1 -1
  9. package/dist/backend/muse.d.ts +26 -0
  10. package/dist/backend/muse.js +131 -0
  11. package/dist/backend/muse.js.map +1 -1
  12. package/dist/channel/adapters/discord.js +12 -1
  13. package/dist/channel/adapters/discord.js.map +1 -1
  14. package/dist/channel/adapters/telegram.js +6 -0
  15. package/dist/channel/adapters/telegram.js.map +1 -1
  16. package/dist/channel/mcp-tools.js +3 -3
  17. package/dist/channel/mcp-tools.js.map +1 -1
  18. package/dist/channel/types.d.ts +3 -0
  19. package/dist/config-validator.js +3 -0
  20. package/dist/config-validator.js.map +1 -1
  21. package/dist/cross-instance-envelope.js +2 -0
  22. package/dist/cross-instance-envelope.js.map +1 -1
  23. package/dist/daemon.d.ts +8 -0
  24. package/dist/daemon.js +126 -54
  25. package/dist/daemon.js.map +1 -1
  26. package/dist/delivery-outbox.d.ts +27 -0
  27. package/dist/delivery-outbox.js +175 -6
  28. package/dist/delivery-outbox.js.map +1 -1
  29. package/dist/fleet-manager.d.ts +40 -1
  30. package/dist/fleet-manager.js +248 -40
  31. package/dist/fleet-manager.js.map +1 -1
  32. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  33. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  34. package/dist/human-delivery-receipts.d.ts +83 -0
  35. package/dist/human-delivery-receipts.js +194 -0
  36. package/dist/human-delivery-receipts.js.map +1 -0
  37. package/dist/instance-config-impact.js +1 -0
  38. package/dist/instance-config-impact.js.map +1 -1
  39. package/dist/instructions.d.ts +1 -1
  40. package/dist/instructions.js +1 -1
  41. package/dist/instructions.js.map +1 -1
  42. package/dist/locale.js +8 -0
  43. package/dist/locale.js.map +1 -1
  44. package/dist/log-service.d.ts +99 -0
  45. package/dist/log-service.js +235 -0
  46. package/dist/log-service.js.map +1 -0
  47. package/dist/log-steps.d.ts +143 -0
  48. package/dist/log-steps.js +646 -0
  49. package/dist/log-steps.js.map +1 -0
  50. package/dist/outbound-handlers.d.ts +3 -0
  51. package/dist/outbound-handlers.js +20 -4
  52. package/dist/outbound-handlers.js.map +1 -1
  53. package/dist/outbound-schemas.d.ts +15 -13
  54. package/dist/outbound-schemas.js +5 -0
  55. package/dist/outbound-schemas.js.map +1 -1
  56. package/dist/public-link-progress.d.ts +1 -1
  57. package/dist/public-link-progress.js +8 -2
  58. package/dist/public-link-progress.js.map +1 -1
  59. package/dist/public-preview.d.ts +2 -1
  60. package/dist/public-preview.js +8 -5
  61. package/dist/public-preview.js.map +1 -1
  62. package/dist/public-web-gateway.js +1 -1
  63. package/dist/public-web-gateway.js.map +1 -1
  64. package/dist/public-web-link.d.ts +4 -1
  65. package/dist/public-web-link.js +42 -13
  66. package/dist/public-web-link.js.map +1 -1
  67. package/dist/reply-buttons-controller.d.ts +3 -0
  68. package/dist/reply-buttons-controller.js +3 -2
  69. package/dist/reply-buttons-controller.js.map +1 -1
  70. package/dist/settings-api.js +4 -0
  71. package/dist/settings-api.js.map +1 -1
  72. package/dist/settings-change.js +2 -0
  73. package/dist/settings-change.js.map +1 -1
  74. package/dist/settings-effect.js +2 -0
  75. package/dist/settings-effect.js.map +1 -1
  76. package/dist/tool-progress.js +3 -2
  77. package/dist/tool-progress.js.map +1 -1
  78. package/dist/topic-commands.d.ts +3 -2
  79. package/dist/topic-commands.js +14 -4
  80. package/dist/topic-commands.js.map +1 -1
  81. package/dist/transcript-jsonl.d.ts +80 -3
  82. package/dist/transcript-jsonl.js +147 -10
  83. package/dist/transcript-jsonl.js.map +1 -1
  84. package/dist/transcript-monitor.d.ts +11 -0
  85. package/dist/transcript-monitor.js +61 -13
  86. package/dist/transcript-monitor.js.map +1 -1
  87. package/dist/transcript-sources.d.ts +24 -0
  88. package/dist/transcript-sources.js +205 -55
  89. package/dist/transcript-sources.js.map +1 -1
  90. package/dist/tunnel/cloudflared.d.ts +7 -15
  91. package/dist/tunnel/cloudflared.js +36 -1
  92. package/dist/tunnel/cloudflared.js.map +1 -1
  93. package/dist/tunnel/types.d.ts +5 -1
  94. package/dist/tunnel/types.js.map +1 -1
  95. package/dist/types.d.ts +6 -0
  96. package/dist/ui/chat-strings.js +8 -2
  97. package/dist/ui/chat-thread.js +24 -4
  98. package/dist/ui/chat-time.js +80 -0
  99. package/dist/ui/panel-log.js +388 -0
  100. package/dist/ui/panel-settings.js +6 -1
  101. package/dist/ui/preview-panel.js +7 -1
  102. package/dist/ui/preview.js +5 -1
  103. package/dist/ui/settings-strings.js +2 -0
  104. package/dist/ui/shared/app-i18n.js +2 -2
  105. package/dist/ui/shared/app-route.js +6 -1
  106. package/dist/ui/shared/app-shell.js +3 -3
  107. package/dist/ui/shared/app-stream.js +2 -1
  108. package/dist/ui/shared/app.css +44 -6
  109. package/dist/ui/shared/app.js +5 -0
  110. package/dist/ui/shared/fonts/README.md +1 -1
  111. package/dist/ui/shared/instance-nav.js +9 -6
  112. package/dist/ui/shared/instance-switch.js +4 -4
  113. package/dist/ui/shared/tokens.css +1 -1
  114. package/dist/web-api.d.ts +5 -0
  115. package/dist/web-api.js +49 -1
  116. package/dist/web-api.js.map +1 -1
  117. package/dist/web-auth.js +2 -1
  118. package/dist/web-auth.js.map +1 -1
  119. package/dist/web-commands.d.ts +2 -2
  120. package/dist/web-commands.js +3 -2
  121. package/dist/web-commands.js.map +1 -1
  122. package/dist/web-reply-files.d.ts +45 -0
  123. package/dist/web-reply-files.js +223 -0
  124. package/dist/web-reply-files.js.map +1 -0
  125. package/dist/web-settings-patch.d.ts +3 -1
  126. package/dist/web-settings-patch.js +4 -1
  127. package/dist/web-settings-patch.js.map +1 -1
  128. package/dist/web-shell-routes.d.ts +4 -0
  129. package/dist/web-shell-routes.js +9 -0
  130. package/dist/web-shell-routes.js.map +1 -1
  131. package/dist/web-upload.d.ts +17 -4
  132. package/dist/web-upload.js +64 -13
  133. package/dist/web-upload.js.map +1 -1
  134. package/package.json +1 -1
@@ -0,0 +1,388 @@
1
+ // #1218 phase 1: one instance's Log (紀錄) — its work step by step: what it was asked, what it answered (never its
2
+ // thinking), each tool call with its arguments and result, errors, where each turn ended, and fleet events (a crash, a
3
+ // restart, a prompt waiting). Between View and Details (/ui/log/<name>). Behind the session gate (/ui/js/); offered on
4
+ // the public link too (the user's decision), read-only, with the same masking.
5
+ //
6
+ // On demand only (the user's decision on load): off by default (web.log_tab) — the tab says so, and offers to turn it
7
+ // on (a Settings write a fleet admin confirms; not over the public link). On, the server reads the agent's transcript
8
+ // only when this page asks (GET /ui/steps), and keeps an eye on it only while this page watches (a lease each read
9
+ // renews): a steps_tick on the stream says it moved, and the page reads after its cursor. Over the public link (no
10
+ // stream) the page asks every POLL_MS while it is shown.
11
+ //
12
+ // Steps are merged by id: a call shown as running is completed in place when its result arrives; older pages are put
13
+ // before. Masking and cutting are the server's (log-steps.ts): the page only shows what it is given.
14
+ import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
15
+ import { register, t } from "/assets/app-i18n.js";
16
+ import { appStore, useStore } from "/assets/app-store.js";
17
+ import { useLease } from "/assets/app-ctx.js";
18
+ import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
19
+ import { InstanceSwitch } from "/assets/instance-switch.js";
20
+ import { HeaderTools } from "/assets/header-tools.js";
21
+ import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
22
+ import { Icon } from "/assets/ui-icons.js";
23
+ import { toast } from "/assets/ui-toast.js";
24
+ import "./chat-strings.js";
25
+ import { chatTime, chatTimeFull } from "./chat-time.js";
26
+ import { confirmedWrite } from "./settings-confirm.js";
27
+
28
+ register("log", {
29
+ en: {
30
+ offTitle: "The Log is off",
31
+ offBody: "Turn it on to see each agent's work step by step: what it was asked, its replies, every tool call with its arguments and result, errors, and fleet events. It reads the agent's own transcript only while this tab is open, and masks secret values.",
32
+ enable: "Turn on the Log", enabling: "Waiting for a fleet admin to confirm it in General…", enabled: "The Log is on.",
33
+ enableFailed: "The Log was not turned on: {0}", offPublic: "A fleet admin can turn it on in Settings → Web.",
34
+ unsupported: "{0} has no transcript AgEnD can read, so this Log shows fleet events only. View shows its screen.",
35
+ coming: "Steps for {0} come in a later update. Fleet events are shown meanwhile.",
36
+ notRunning: "Its transcript is found while it runs. Start it to see its steps; fleet events are shown meanwhile.",
37
+ noTranscript: "No transcript yet: steps appear once it has worked.",
38
+ empty: "Nothing here yet.", olderLoad: "Load older steps", olderStart: "The start of what can be read.", loading: "Loading…",
39
+ all: "All", tools: "Tools", errors: "Errors", filter: "Show", live: "Live", reset: "A new conversation began: showing it from its latest steps.",
40
+ user: "Asked", assistant: "Replied", error: "Error", errorCode: "Error {0}", turnEnd: "Turn ended", turnEndIn: "Turn ended · {0}",
41
+ compacted: "Conversation compacted", turn: "Turn",
42
+ running: "running", done: "done", failed: "failed", interrupted: "interrupted", exit: "exit {0}",
43
+ showOutput: "Show output", hideOutput: "Hide output", args: "Arguments", output: "Output", noOutput: "No output.",
44
+ showAll: "Show all", showLess: "Show less", cut: "… {0} more characters not shown.", tooLarge: "Output of {0}: only its start is shown.",
45
+ times: "×{0}", newer: "{0} new",
46
+ cat_read: "Read", cat_edit: "Edit", cat_search: "Search", cat_run: "Run", cat_web: "Web", cat_mcp: "MCP", cat_agent: "Sub-agent",
47
+ cat_plan: "Plan", cat_message: "Message", cat_other: "Tool", plan_items: "{0} items",
48
+ ev_crash_respawn: "Crashed and restarted", ev_crash_loop: "Keeps crashing", ev_hang_detected: "Looks stuck", ev_hang_action: "Stuck: acted on",
49
+ ev_instance_paused: "Paused", ev_instance_resumed: "Resumed", ev_interactive_prompt: "Waiting at a prompt", ev_interactive_prompt_assist: "Prompt: asked for help",
50
+ ev_dialog_parked: "A dialog waits", ev_model_failover: "Switched to another model", ev_model_recovered: "Model back",
51
+ ev_mcp_died: "Tool server stopped", ev_mcp_recovered: "Tool server back", ev_mcp_auto_restart: "Tool server restarted",
52
+ ev_message_failed: "A message was not delivered", ev_pty_error: "Terminal error", ev_pty_recovered: "Terminal recovered",
53
+ ev_backend_unreachable: "CLI unreachable", ev_startup_backend_unreachable: "CLI unreachable at start", ev_backend_launch_warning: "Launch warning",
54
+ ev_clear_action: "Conversation cleared", ev_normal_exit_action: "Exited", ev_reply_drop_detected: "A reply went missing",
55
+ ev_reply_drop_recovered: "Missing reply recovered", ev_reply_drop_unrecovered: "Missing reply not recovered",
56
+ ev_supervision_ended: "Supervision ended", ev_tmux_server_crash: "tmux crashed", ev_malformed_tool_call: "Malformed tool call", ev_health_check_error: "Health check error",
57
+ },
58
+ "zh-TW": {
59
+ offTitle: "紀錄未開啟",
60
+ offBody: "開啟後可逐步看到每個 agent 的工作:收到的要求、回覆、每次工具呼叫的參數與結果、錯誤,以及 fleet 事件。只在這個分頁開著時讀取 agent 自己的對話紀錄,機密值會遮蔽。",
61
+ enable: "開啟紀錄", enabling: "等待 fleet admin 在 General 確認…", enabled: "紀錄已開啟。",
62
+ enableFailed: "紀錄沒有開啟:{0}", offPublic: "fleet admin 可以在「設定 → 網頁」開啟。",
63
+ unsupported: "{0} 沒有 AgEnD 讀得到的對話紀錄,所以這裡只顯示 fleet 事件。「檢視」可以看它的畫面。",
64
+ coming: "{0} 的步驟會在之後的更新加入,目前先顯示 fleet 事件。",
65
+ notRunning: "它執行時才找得到對話紀錄。啟動它就能看到步驟;目前先顯示 fleet 事件。",
66
+ noTranscript: "還沒有對話紀錄:它開始工作後就會出現步驟。",
67
+ empty: "目前還沒有內容。", olderLoad: "載入更早的步驟", olderStart: "已經是能讀到的最早內容。", loading: "載入中…",
68
+ all: "全部", tools: "工具", errors: "錯誤", filter: "顯示", live: "即時", reset: "開始了新的對話:從它最新的步驟顯示。",
69
+ user: "收到要求", assistant: "回覆", error: "錯誤", errorCode: "錯誤 {0}", turnEnd: "回合結束", turnEndIn: "回合結束 · {0}",
70
+ compacted: "對話已壓縮", turn: "回合",
71
+ running: "執行中", done: "完成", failed: "失敗", interrupted: "中斷", exit: "結束碼 {0}",
72
+ showOutput: "顯示輸出", hideOutput: "隱藏輸出", args: "參數", output: "輸出", noOutput: "沒有輸出。",
73
+ showAll: "顯示全部", showLess: "收合", cut: "… 還有 {0} 個字元沒有顯示。", tooLarge: "輸出共 {0},只顯示開頭。",
74
+ times: "×{0}", newer: "{0} 則新的",
75
+ cat_read: "讀取", cat_edit: "編輯", cat_search: "搜尋", cat_run: "執行", cat_web: "網頁", cat_mcp: "MCP", cat_agent: "子 agent",
76
+ cat_plan: "計畫", cat_message: "訊息", cat_other: "工具", plan_items: "{0} 項",
77
+ ev_crash_respawn: "當機後重新啟動", ev_crash_loop: "反覆當機", ev_hang_detected: "看起來卡住了", ev_hang_action: "卡住:已處理",
78
+ ev_instance_paused: "已暫停", ev_instance_resumed: "已繼續", ev_interactive_prompt: "停在提示上等待", ev_interactive_prompt_assist: "提示:已請求協助",
79
+ ev_dialog_parked: "有對話框在等待", ev_model_failover: "改用另一個模型", ev_model_recovered: "模型恢復",
80
+ ev_mcp_died: "工具伺服器停止", ev_mcp_recovered: "工具伺服器恢復", ev_mcp_auto_restart: "工具伺服器已重新啟動",
81
+ ev_message_failed: "有訊息沒有送達", ev_pty_error: "終端機錯誤", ev_pty_recovered: "終端機恢復",
82
+ ev_backend_unreachable: "連不到 CLI", ev_startup_backend_unreachable: "啟動時連不到 CLI", ev_backend_launch_warning: "啟動警告",
83
+ ev_clear_action: "對話已清除", ev_normal_exit_action: "已結束", ev_reply_drop_detected: "有回覆遺失",
84
+ ev_reply_drop_recovered: "遺失的回覆已補回", ev_reply_drop_unrecovered: "遺失的回覆沒有補回",
85
+ ev_supervision_ended: "監督結束", ev_tmux_server_crash: "tmux 當掉", ev_malformed_tool_call: "格式錯誤的工具呼叫", ev_health_check_error: "健康檢查錯誤",
86
+ },
87
+ });
88
+ const tl = (key, ...a) => t(`log.${key}`, ...a);
89
+
90
+ /** The public link has no stream: the page asks this often while it is shown. */
91
+ export const POLL_MS = 4000;
92
+ /** A watching page renews its lease at least this often (the server's lease is 45 s). */
93
+ export const RENEW_MS = 30_000;
94
+ /** Steps kept in the page; older ones beyond it are dropped from the top (the server still has them). */
95
+ export const PAGE_MAX = 1000;
96
+ /** A text longer than this many lines is folded until opened. */
97
+ const FOLD_LINES = 8;
98
+
99
+ // The app's own SVG icons (never emoji: a device without an emoji font shows boxes).
100
+ const CAT_ICON = { read: "file", edit: "edit", search: "search", run: "code", web: "globe", mcp: "plug", agent: "team", plan: "tasks", message: "send", other: "sliders" };
101
+ const KIND_ICON = { user: "user", assistant: "bot", error: "alert", turn_end: "stop", state: "info", event: "bell" };
102
+
103
+ function size(bytes) {
104
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
105
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
106
+ return `${bytes} B`;
107
+ }
108
+ function duration(ms) {
109
+ if (!(ms >= 0)) return "";
110
+ if (ms < 1000) return `${ms} ms`;
111
+ const s = ms / 1000;
112
+ if (s < 60) return `${s < 10 ? s.toFixed(1) : Math.round(s)} s`;
113
+ const m = Math.floor(s / 60);
114
+ return `${m} m ${Math.round(s - m * 60)} s`;
115
+ }
116
+ const eventLabel = (type) => { const k = `log.ev_${type}`; const v = t(k); return v === k ? String(type).replace(/_/g, " ") : v; };
117
+
118
+ /**
119
+ * Merge `incoming` steps into `list` (array, in order): one with an id already there replaces it in place (a result
120
+ * completing its call); a new one is added — at the end, or before everything when `older`. Returns a new array.
121
+ */
122
+ export function mergeSteps(list, incoming, older = false) {
123
+ const at = new Map(list.map((s, i) => [s.id, i]));
124
+ const next = list.slice();
125
+ const added = [];
126
+ for (const s of incoming) {
127
+ const i = at.get(s.id);
128
+ if (i !== undefined) {
129
+ // An older page never overwrites what a newer read already said about the same step — except to name a call the
130
+ // newer page knew only by its result (the call's record was older than that page): its tool, subject and input.
131
+ // A newer read that saw only a call's result (the call was before where it read) never unnames the call: its tool,
132
+ // category and start time stay; the outcome is the newer read's.
133
+ if (!older) next[i] = s.kind === "tool" && !s.tool && next[i].tool
134
+ ? { ...next[i], ...s, tool: next[i].tool, cat: next[i].cat, ts: next[i].ts }
135
+ : { ...next[i], ...s };
136
+ else if (next[i].kind === "tool" && !next[i].tool && s.tool) {
137
+ const { tool, cat, subject, input, ts } = s;
138
+ next[i] = { ...next[i], tool, cat, ...(subject !== undefined ? { subject } : {}), ...(input !== undefined ? { input } : {}), ...(ts ? { ts } : {}) };
139
+ }
140
+ } else { at.set(s.id, -1); added.push(s); }
141
+ }
142
+ return older ? [...added, ...next] : [...next, ...added];
143
+ }
144
+
145
+ /** Steps and fleet events in one timeline: events placed by time among the steps (steps keep their file order). */
146
+ export function timeline(steps, events) {
147
+ const evs = events.slice().sort((a, b) => a.ts - b.ts || a.id - b.id);
148
+ const out = [];
149
+ let e = 0;
150
+ for (const s of steps) {
151
+ while (e < evs.length && s.ts > 0 && evs[e].ts <= s.ts) out.push({ kind: "event", ...evs[e++] });
152
+ out.push(s);
153
+ }
154
+ while (e < evs.length) out.push({ kind: "event", ...evs[e++] });
155
+ return out;
156
+ }
157
+
158
+ const isError = (x) => x.kind === "error" || x.kind === "event" || x.status === "failed" || x.status === "interrupted";
159
+ /** Consecutive identical finished tool steps (same tool, subject and status) as one row with a count: a loop shows. */
160
+ export function collapse(items) {
161
+ const out = [];
162
+ for (const x of items) {
163
+ const prev = out[out.length - 1];
164
+ if (prev && x.kind === "tool" && prev.kind === "tool" && x.status !== "running" && prev.status === x.status
165
+ && prev.tool === x.tool && (prev.subject || "") === (x.subject || "") && prev.turn === x.turn) {
166
+ prev.repeat = (prev.repeat || 1) + 1;
167
+ continue;
168
+ }
169
+ out.push({ ...x });
170
+ }
171
+ return out;
172
+ }
173
+
174
+ export function LogPanel({ route, navKey }) {
175
+ const app = useStore(appStore);
176
+ const name = route.instance;
177
+ const inst = app.instances.find((i) => i.name === name) || null;
178
+ useEffect(() => { setTitle(`${name} · ${t("app.log")}`); }, [name, navKey]);
179
+ if (!app.ready) return html`<div class="panel p-log"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
180
+ if (!inst) {
181
+ return html`<div class="panel p-log"><${PanelHeader} title=${name} />
182
+ <div class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
183
+ action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
184
+ }
185
+ return html`<${LogView} key=${name} name=${name} inst=${inst} navKey=${navKey} exec=${app.exec[name]}
186
+ awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, name) ? app.awaiting[name] : null} />`;
187
+ }
188
+
189
+ function LogView({ name, inst, navKey, exec, awaiting }) {
190
+ const lease = useLease(`log:${name}`);
191
+ const [state, setState] = useState({ phase: "loading" }); // loading | off | on | failed
192
+ const [steps, setSteps] = useState([]);
193
+ const [events, setEvents] = useState([]);
194
+ const [info, setInfo] = useState(null); // { backend, supported, reason }
195
+ const [filter, setFilter] = useState("all");
196
+ const [older, setOlder] = useState({ busy: false });
197
+ const [notice, setNotice] = useState("");
198
+ const [enabling, setEnabling] = useState(false);
199
+ const cursor = useRef({ key: null, after: null, skip: false, before: null, ev: 0 });
200
+ const reading = useRef(false), again = useRef(false);
201
+ const scroller = useRef(null), pinned = useRef(true);
202
+ const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
203
+ const polling = globalThis.document?.body?.dataset?.webTransport === "poll";
204
+
205
+ const q = (params) => `/ui/steps?${new URLSearchParams({ instance: name, ...params })}`;
206
+ const read = async (url) => {
207
+ const r = await lease.fetch(url, { cache: "no-store" });
208
+ if (!lease.current()) return null;
209
+ const j = await r.json().catch(() => null);
210
+ if (!lease.current()) return null;
211
+ if (!r.ok || !j) throw new Error((j && j.error) || String(r.status));
212
+ return j;
213
+ };
214
+ const take = (j, { replace = false } = {}) => {
215
+ if (!j.enabled) { setState({ phase: "off" }); return; }
216
+ setInfo({ backend: j.backend, supported: j.supported, reason: j.reason || null });
217
+ const c = cursor.current;
218
+ if (j.key) c.key = j.key;
219
+ if (j.after !== null && j.after !== undefined) { c.after = j.after; c.skip = !!j.skip; }
220
+ if (replace) c.before = j.before ?? null;
221
+ if (typeof j.ev === "number") c.ev = Math.max(c.ev, j.ev);
222
+ setSteps((list) => {
223
+ const merged = replace ? j.steps || [] : mergeSteps(list, j.steps || []);
224
+ return merged.length > PAGE_MAX ? merged.slice(merged.length - PAGE_MAX) : merged;
225
+ });
226
+ if (j.events && j.events.length) setEvents((list) => (replace ? j.events : [...list, ...j.events.filter((e) => !list.some((x) => x.id === e.id))]));
227
+ else if (replace) setEvents([]);
228
+ if (j.reset) setNotice(tl("reset"));
229
+ setState({ phase: "on" });
230
+ };
231
+
232
+ // The newest window (and the watch), once per page and on Retry.
233
+ const open = async () => {
234
+ reading.current = true;
235
+ try {
236
+ const j = await read(q({ watch: "1" }));
237
+ if (j) take(j, { replace: true });
238
+ } catch { if (lease.current()) setState({ phase: "failed" }); }
239
+ finally { reading.current = false; }
240
+ };
241
+ // What came after the cursor: one read at a time, and once more if asked meanwhile.
242
+ const pull = async () => {
243
+ if (state.phase !== "on" && state.phase !== "loading") return;
244
+ if (reading.current) { again.current = true; return; }
245
+ const c = cursor.current;
246
+ if (c.after === null && c.key === null) return void open();
247
+ reading.current = true;
248
+ try {
249
+ const params = { watch: "1", ev: String(c.ev) };
250
+ if (c.after !== null) params.after = String(c.after);
251
+ if (c.key) params.key = c.key;
252
+ if (c.skip) params.skip = "1";
253
+ const j = await read(q(params));
254
+ if (j) take(j, { replace: !!j.reset });
255
+ } catch { /* the next tick or poll tries again */ }
256
+ finally {
257
+ reading.current = false;
258
+ if (again.current && lease.current()) { again.current = false; void pull(); }
259
+ }
260
+ };
261
+ const loadOlder = async () => {
262
+ const c = cursor.current;
263
+ if (c.before === null || older.busy) return;
264
+ setOlder({ busy: true });
265
+ try {
266
+ const j = await read(q({ before: String(c.before), ...(c.key ? { key: c.key } : {}) }));
267
+ if (j && j.enabled) {
268
+ if (j.reset) { take(j, { replace: true }); return; }
269
+ c.before = j.before ?? null;
270
+ const el = scroller.current, from = el ? el.scrollHeight - el.scrollTop : 0;
271
+ setSteps((list) => mergeSteps(list, j.steps || [], true));
272
+ // Keep the reader where they were: what was at the top stays in view.
273
+ requestAnimationFrame(() => { if (el && lease.current()) el.scrollTop = el.scrollHeight - from; });
274
+ }
275
+ } catch { toast(t("chat.loadFailed"), false); }
276
+ finally { if (lease.current()) setOlder({ busy: false }); }
277
+ };
278
+
279
+ useEffect(() => { void open(); }, [lease]);
280
+ // Live: the stream's tick for this instance; over the public link, a poll while shown. A renewal keeps the lease.
281
+ useEffect(() => {
282
+ if (state.phase !== "on") return;
283
+ const on = appStore.get().streamOn;
284
+ if (on && !polling) lease.hold(on("steps_tick", (d) => { if (d && d.instance === name) void pull(); }));
285
+ const shown = () => !globalThis.document?.hidden;
286
+ if (polling) lease.interval(() => { if (shown()) void pull(); }, POLL_MS);
287
+ lease.interval(() => { if (shown()) void pull(); }, RENEW_MS);
288
+ // Coming back to a hidden tab: catch up at once.
289
+ lease.on(globalThis.document, "visibilitychange", () => { if (shown()) void pull(); });
290
+ }, [lease, state.phase]);
291
+
292
+ // Pinned to the newest step, like the chat: a reader who scrolled up is never moved.
293
+ const onScroll = () => { const el = scroller.current; if (el) pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 40; };
294
+ useLayoutEffect(() => { const el = scroller.current; if (el && pinned.current) el.scrollTop = el.scrollHeight; }, [steps, events, filter]);
295
+
296
+ const enable = async () => {
297
+ if (enabling) return;
298
+ setEnabling(true);
299
+ try {
300
+ const res = await confirmedWrite("/api/settings/fleet/web", { method: "PUT", body: { log_tab: true }, label: tl("enable") });
301
+ if (!lease.current()) return;
302
+ if (res.ok) { toast(tl("enabled")); setState({ phase: "loading" }); void open(); }
303
+ else toast(tl("enableFailed", (res.body && res.body.error) || String(res.status)), false);
304
+ } catch (err) { toast(tl("enableFailed", err && err.message ? err.message : "?"), false); }
305
+ finally { if (lease.current()) setEnabling(false); }
306
+ };
307
+
308
+ const cls = statusClass(inst, exec, awaiting);
309
+ const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>`;
310
+ const items = useMemo(() => {
311
+ const all = timeline(steps, events);
312
+ const shown = filter === "tools" ? all.filter((x) => x.kind === "tool") : filter === "errors" ? all.filter(isError) : all;
313
+ return collapse(shown);
314
+ }, [steps, events, filter]);
315
+
316
+ let content;
317
+ if (state.phase === "failed") content = html`<${ErrorState} message=${t("chat.loadFailed")} onRetry=${() => { setState({ phase: "loading" }); void open(); }} />`;
318
+ else if (state.phase === "loading") content = html`<${Skeleton} lines=${6} />`;
319
+ else if (state.phase === "off") {
320
+ content = html`<div class="log-off center"><${Empty} icon="tasks" title=${tl("offTitle")} hint=${tl("offBody")}
321
+ action=${overPublicLink ? html`<p class="note">${tl("offPublic")}</p>`
322
+ : html`<button type="button" class="btn btn-primary log-enable" disabled=${enabling} onClick=${enable}>${enabling ? tl("enabling") : tl("enable")}</button>`} /></div>`;
323
+ } else {
324
+ const why = info && info.reason ? (info.reason === "unsupported" ? tl("unsupported", info.backend) : info.reason === "coming" ? tl("coming", info.backend)
325
+ : info.reason === "not_running" ? tl("notRunning") : info.reason === "no_transcript" ? tl("noTranscript") : "") : "";
326
+ content = html`
327
+ ${why ? html`<p class="note log-why" role="status">${why}</p>` : null}
328
+ ${notice ? html`<p class="note log-notice" role="status">${notice}</p>` : null}
329
+ ${cursor.current.before !== null
330
+ ? html`<div class="log-older"><button type="button" class="btn btn-sm" disabled=${older.busy} onClick=${loadOlder}>${older.busy ? tl("loading") : tl("olderLoad")}</button></div>`
331
+ : steps.length ? html`<p class="note log-start">${tl("olderStart")}</p>` : null}
332
+ ${items.length ? html`<ol class="log-list" role="log" aria-label=${t("app.log")}>${items.map((x, i) => html`<${Row} key=${`${x.kind}:${x.id}`} x=${x} prev=${items[i - 1]} />`)}</ol>`
333
+ : html`<p class="note log-empty">${tl("empty")}</p>`}`;
334
+ }
335
+ const tools = state.phase === "on" ? html`<div class="log-bar" role="group" aria-label=${tl("filter")}>
336
+ ${["all", "tools", "errors"].map((k) => html`<button key=${k} type="button" class=${`chip-btn${filter === k ? " on" : ""}`} aria-pressed=${filter === k ? "true" : "false"} onClick=${() => setFilter(k)}>${tl(k)}</button>`)}
337
+ <span class="grow"></span><span class="log-live" title=${tl("live")}><span class="dot running" aria-hidden="true"></span>${tl("live")}</span></div>` : null;
338
+ return html`<div class="panel p-log">
339
+ <${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="log" />`}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
340
+ ${tools}
341
+ <div class="panel-body log-body" ref=${scroller} onScroll=${onScroll}><div class="col log-col">${content}</div></div>
342
+ </div>`;
343
+ }
344
+
345
+ /** One line of the Log. */
346
+ function Row({ x, prev }) {
347
+ const [open, setOpen] = useState(false);
348
+ const [full, setFull] = useState(false);
349
+ const time = x.ts ? chatTime(x.ts, { yesterday: (hm) => t("chat.yesterdayAt", hm) }) : "";
350
+ const title = x.ts ? chatTimeFull(x.ts) : "";
351
+ const newTurn = x.kind === "user" && prev && prev.turn !== x.turn;
352
+ const head = (icon, label, extra) => html`<div class="log-head"><span class="log-icon" aria-hidden="true"><${Icon} name=${icon} size=${14} /></span>
353
+ <span class="log-label">${label}</span>${extra || null}<span class="grow"></span><span class="log-time" title=${title}>${time}</span></div>`;
354
+ const text = (body, cut, bytes) => {
355
+ if (!body) return null;
356
+ const lines = body.split("\n").length;
357
+ const folded = !full && lines > FOLD_LINES;
358
+ return html`<div class=${`log-text${folded ? " folded" : ""}`}>${body}</div>
359
+ ${lines > FOLD_LINES ? html`<button type="button" class="link-btn log-more" onClick=${() => setFull(!full)}>${full ? tl("showLess") : tl("showAll")}</button>` : null}
360
+ ${cut ? html`<p class="note">${tl("cut", cut.toLocaleString())}</p>` : null}
361
+ ${bytes ? html`<p class="note">${tl("tooLarge", size(bytes))}</p>` : null}`;
362
+ };
363
+ let row;
364
+ if (x.kind === "event") {
365
+ row = html`${head(KIND_ICON.event, eventLabel(x.type))}${x.detail ? html`<div class="log-text muted">${x.detail}</div>` : null}`;
366
+ } else if (x.kind === "user" || x.kind === "assistant") {
367
+ row = html`${head(KIND_ICON[x.kind], tl(x.kind))}${text(x.body, x.bodyCut)}`;
368
+ } else if (x.kind === "error") {
369
+ row = html`${head(KIND_ICON.error, x.note ? tl("errorCode", x.note) : tl("error"))}${text(x.body, x.bodyCut)}`;
370
+ } else if (x.kind === "turn_end") {
371
+ row = head(KIND_ICON.turn_end, x.durationMs ? tl("turnEndIn", duration(x.durationMs)) : tl("turnEnd"),
372
+ x.status && x.status !== "done" ? html`<span class=${`log-status ${x.status}`}>${tl(x.status)}</span>` : null);
373
+ } else if (x.kind === "state") {
374
+ row = head(KIND_ICON.state, x.note === "compacted" ? tl("compacted") : x.note || "");
375
+ } else {
376
+ const cat = x.cat || "other";
377
+ const subject = cat === "plan" && x.subject ? tl("plan_items", x.subject) : x.subject || "";
378
+ const status = html`<span class=${`log-status ${x.status || "done"}`}>${x.status === "running" ? html`<span class="spinner" aria-hidden="true"></span>` : null}${tl(x.status || "done")}${typeof x.exitCode === "number" && x.exitCode !== 0 ? ` · ${tl("exit", x.exitCode)}` : ""}${x.durationMs ? ` · ${duration(x.durationMs)}` : ""}</span>`;
379
+ const label = html`<span class="log-cat">${tl(`cat_${cat}`)}</span>${subject ? html` <span class="log-subject mono">${subject}</span>` : null}${x.tool && !subject ? html` <span class="log-tool mono muted">${x.tool}</span>` : null}`;
380
+ const has = !!(x.body || x.input || x.bodyBytes);
381
+ row = html`${head(CAT_ICON[cat] || CAT_ICON.other, label, html`${x.repeat ? html`<span class="log-repeat">${tl("times", x.repeat)}</span>` : null}${status}`)}
382
+ ${has ? html`<button type="button" class="link-btn log-toggle" aria-expanded=${open ? "true" : "false"} onClick=${() => setOpen(!open)}>${open ? tl("hideOutput") : tl("showOutput")}</button>` : null}
383
+ ${open ? html`<div class="log-detail">
384
+ ${x.input ? html`<div class="log-sec"><span class="log-sec-h">${tl("args")}</span><div class="log-text mono">${x.input}</div>${x.inputCut ? html`<p class="note">${tl("cut", x.inputCut.toLocaleString())}</p>` : null}</div>` : null}
385
+ <div class="log-sec"><span class="log-sec-h">${tl("output")}</span>${x.body ? text(x.body, x.bodyCut, x.bodyBytes) : html`<p class="note">${tl("noOutput")}</p>`}</div></div>` : null}`;
386
+ }
387
+ return html`<li class=${`log-item k-${x.kind}${x.status ? ` s-${x.status}` : ""}${newTurn ? " turn-start" : ""}`}>${row}</li>`;
388
+ }
@@ -718,13 +718,14 @@ function WebSection({ ctx, staged }) {
718
718
  const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
719
719
  const original = useMemo(() => ({
720
720
  echo: web.echo_to_channel ?? true,
721
+ log: web.log_tab === true, // #1218: off by default
721
722
  pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" },
722
723
  session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
723
724
  terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
724
725
  }), []);
725
726
  const text = (v) => (v === null || v === undefined ? "" : String(v));
726
727
  const [f, setF] = useState(() => ({
727
- echo: original.echo, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
728
+ echo: original.echo, log: original.log, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
728
729
  abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
729
730
  termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
730
731
  }));
@@ -752,12 +753,14 @@ function WebSection({ ctx, staged }) {
752
753
  ttl_minutes: value(f.termTtl) }, original.terminal);
753
754
  const body = {
754
755
  ...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
756
+ ...(f.log !== original.log ? { log_tab: f.log } : {}),
755
757
  ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
756
758
  ...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
757
759
  ...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
758
760
  };
759
761
  const fields = [
760
762
  ...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
763
+ ...(body.log_tab !== undefined ? ["web.log_tab"] : []),
761
764
  ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
762
765
  ...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
763
766
  ...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
@@ -797,6 +800,8 @@ function WebSection({ ctx, staged }) {
797
800
  <section class="card"><h3>${tn("webChat")}</h3>
798
801
  <label class="check"><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
799
802
  <p class="note">${tn("webEchoHint")}</p>
803
+ <label class="check"><input id="webLogTab" type="checkbox" checked=${f.log} onChange=${(e) => set("log")(e.target.checked)} /><span>${tn("webLogTab")}</span><${Impact} schema=${schema} field="web.log_tab" /></label>
804
+ <p class="note">${tn("webLogTabHint")}</p>
800
805
  <label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
801
806
  <p class="note">${tn("publicLinkHint")}</p>
802
807
  <div class="grid2">
@@ -16,6 +16,7 @@ import { Icon } from "/assets/ui-icons.js";
16
16
  import "./chat-render.js";
17
17
  import "./preview.js";
18
18
  import { loadHtmlAttachment } from "./html-attachment.js";
19
+ import { chatTime, onDayChange } from "./chat-time.js";
19
20
 
20
21
  const R = () => globalThis.AgendChatRender;
21
22
  const P = () => globalThis.AgendPreview;
@@ -112,7 +113,8 @@ export function clampWidth(w, total) {
112
113
  return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
113
114
  }
114
115
 
115
- const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
116
+ /** #1770: as the thread shows it — the fleet's zone, with the day when it is not today. */
117
+ const timeOf = (ts) => (ts ? chatTime(ts, { yesterday: (hm) => t("chat.yesterdayAt", hm) }) : "");
116
118
 
117
119
  /**
118
120
  * The panel, inside the chat's split (`split`: that element — the divider sets its width there, through CSSOM).
@@ -140,6 +142,10 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
140
142
  useEffect(() => (P() ? P().onChange(() => { if (pending.current && ownAllow.current !== pending.current) pending.current = 0; choice(n => n + 1); }) : undefined), []);
141
143
  // Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
142
144
  useEffect(() => () => closePanel(), []);
145
+ // #1770: the source and newer-version labels say the day as the thread does — drawn again when the fleet's day
146
+ // changes, while a block is open. A render: the frame and the run are left as they are.
147
+ const [, dayTick] = useState(0);
148
+ useEffect(() => (open ? onDayChange(() => dayTick(n => n + 1)) : undefined), [!!open]);
143
149
 
144
150
  const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" ? s : "idle", reason: reason || "" }) };
145
151
  const start = () => {
@@ -31,7 +31,11 @@
31
31
  var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "", detail: "" };
32
32
  // Why it failed, in English when the app's dictionary has no word for it (chat-strings.js pvFail_*).
33
33
  var FAIL_EN = { exited: "its address closed unexpectedly", "spawn-failed": "cloudflared could not start for it", "no-address": "no address was given for it",
34
- unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start" };
34
+ unreachable: "its address could not be reached", "lease-held": "an earlier preview tunnel is still being cleaned up", "gateway-failed": "its local listener could not start",
35
+ // #1767: what cloudflared's exit before an address really was.
36
+ "rate-limited": "Cloudflare is limiting new temporary links from this machine — try again in a while",
37
+ "provisioning-failed": "Cloudflare refused to create its address right now", "process-exited": "cloudflared stopped before it gave an address",
38
+ "bad-address": "Cloudflare gave an address that could not be used" };
35
39
  // #1554: the page's words. `say(key, english, …values)` asks the app's dictionary (init's opts.text) and keeps the
36
40
  // English here when it has none — so this file still works, and reads the same, without the app.
37
41
  var text = null;
@@ -15,6 +15,7 @@ register("settings", {
15
15
  publicProtocol: "Tunnel protocol",
16
16
  webEcho: "Web echo",
17
17
  webEchoHint: "Copies web messages to the bound Telegram or Discord topic. ClassicBot rooms are excluded.",
18
+ webLogTab: "Log tab: each agent's steps", webLogTabHint: "Shows each instance's prompts, replies, tool calls and results, and fleet events in its Log tab, read from its own transcript only while the tab is open. Secret values are masked. Also over the public link. Turning it on waits for a fleet admin's confirmation.",
18
19
  general: "General Settings",
19
20
  advanced: "Advanced",
20
21
  new: "New",
@@ -405,6 +406,7 @@ register("settings", {
405
406
  publicProtocol: "Tunnel 協定",
406
407
  webEcho: "Web 同步",
407
408
  webEchoHint: "將網頁訊息複製到綁定的 Telegram 或 Discord 主題,不包含 ClassicBot 群組。",
409
+ webLogTab: "紀錄分頁:每個 agent 的步驟", webLogTabHint: "在每個 instance 的「紀錄」分頁顯示提示、回覆、工具呼叫與結果,以及 fleet 事件;只在分頁開著時從它自己的對話紀錄讀取。機密值會遮蔽。公開連結也看得到。開啟需 fleet admin 確認。",
408
410
  general: "一般設定",
409
411
  advanced: "進階",
410
412
  new: "新增",
@@ -66,7 +66,7 @@ register("app", {
66
66
  names: "Names", namesNickname: "Nickname first", namesInstance: "Real name first",
67
67
  theme: "Theme", themeSystem: "System", themeLight: "Light", themeDark: "Dark", language: "Language",
68
68
  previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
69
- needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
69
+ needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", log: "Log", instanceViews: "Views of {0}",
70
70
  textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage", usageHide: "Hide usage",
71
71
  statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
72
72
  connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
@@ -97,7 +97,7 @@ register("app", {
97
97
  names: "名稱", namesNickname: "暱稱優先", namesInstance: "實際名稱優先",
98
98
  theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
99
99
  previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
100
- needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
100
+ needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
101
101
  textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量", usageHide: "隱藏用量",
102
102
  statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
103
103
  connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
@@ -13,7 +13,7 @@ export function isSafeInstanceName(name) {
13
13
  }
14
14
 
15
15
  /**
16
- * A path → the route it names: { panel: "chat" | "view" | "details", instance } | { panel: "fleet", tab } |
16
+ * A path → the route it names: { panel: "chat" | "view" | "details" | "log", instance } | { panel: "fleet", tab } |
17
17
  * { panel: "settings", section } | null (not a page of the app: a full load). A malformed name is null too; the
18
18
  * server answers it 400.
19
19
  */
@@ -28,6 +28,9 @@ export function parseRoute(pathname) {
28
28
  if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : null;
29
29
  m = /^\/ui\/chat\/([^/]+)$/.exec(pathname);
30
30
  if (m) return named("chat", m[1]);
31
+ // #1218: one instance's Log.
32
+ m = /^\/ui\/log\/([^/]+)$/.exec(pathname);
33
+ if (m) return named("log", m[1]);
31
34
  if (pathname === "/view") return { panel: "view", instance: null };
32
35
  m = /^\/view\/([^/]+)$/.exec(pathname);
33
36
  if (m) return named("view", m[1]);
@@ -45,6 +48,7 @@ function named(panel, segment) {
45
48
  export function chatPath(instance) { return `/ui/chat/${encodeURIComponent(instance)}`; }
46
49
  export function viewPath(instance) { return instance ? `/view/${encodeURIComponent(instance)}` : "/view"; }
47
50
  export function detailsPath(instance) { return `/ui/fleet/agent/${encodeURIComponent(instance)}`; }
51
+ export function logPath(instance) { return `/ui/log/${encodeURIComponent(instance)}`; }
48
52
  export function fleetPath(tab) { return tab && tab !== "tasks" ? `/ui/fleet/${tab}` : "/ui/fleet"; }
49
53
  export const NEEDS_PATH = "/ui/needs";
50
54
  export function settingsPath(section) { return section && section !== "agents" ? `/settings/${section}` : "/settings"; }
@@ -55,6 +59,7 @@ export function routePath(route) {
55
59
  if (route.panel === "needs") return NEEDS_PATH;
56
60
  if (route.panel === "view") return viewPath(route.instance);
57
61
  if (route.panel === "details") return detailsPath(route.instance);
62
+ if (route.panel === "log") return logPath(route.instance);
58
63
  return route.instance ? chatPath(route.instance) : "/ui";
59
64
  }
60
65
  /** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */
@@ -348,11 +348,11 @@ export function PanelHeader({ title, sub, children, headingRef, nav }) {
348
348
  }
349
349
 
350
350
  /**
351
- * #1523 N2: the instance a page is about — Chat, View or Details of it — or null. With one open, Fleet goes to its
351
+ * #1523 N2: the instance a page is about — Chat, View, Log (#1218) or Details of it — or null. With one open, Fleet goes to its
352
352
  * Details (the user's complaint: Fleet always landed on Tasks), and Chat and View keep it; with none, as before.
353
353
  */
354
354
  export function openInstanceOf(route) {
355
- return route && (route.panel === "chat" || route.panel === "view" || route.panel === "details") && route.instance ? route.instance : null;
355
+ return route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") && route.instance ? route.instance : null;
356
356
  }
357
357
 
358
358
  function BottomTabs({ route, viewOnly }) {
@@ -366,7 +366,7 @@ function BottomTabs({ route, viewOnly }) {
366
366
  <${Icon} name=${icon} size=${20} /><span>${label}</span>${badge ? html`<span class="tab-badge" aria-label=${badgeLabel || t("app.needsYou")}>${badge}</span>` : null}</a>`;
367
367
  let lastView = null;
368
368
  try { lastView = localStorage.getItem("agend_last_view"); } catch { /* none */ }
369
- const viewHref = route && (route.panel === "view" || route.panel === "details") && route.instance ? viewPath(route.instance) : viewPath(lastView);
369
+ const viewHref = route && (route.panel === "view" || route.panel === "log" || route.panel === "details") && route.instance ? viewPath(route.instance) : viewPath(lastView);
370
370
  if (viewOnly) {
371
371
  return html`<nav class="tabs" aria-label=${t("app.menu")}>
372
372
  ${tab(viewHref, "view", t("app.view"), route && route.panel === "view")}
@@ -29,7 +29,8 @@
29
29
  // - a 401 is the session ending: nothing is retried (agend-auth.js says so).
30
30
  // - suspend()/resume(): the page is leaving (pagehide) / came back from the back-forward cache.
31
31
 
32
- const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons"];
32
+ // #1218: steps_tick — an instance's transcript moved while a Log watches it (SSE only: a poll page asks the Log itself).
33
+ const EVENTS = ["status", "message", "delivery", "deliveries", "prompt", "prompts", "prompt_resolved", "activity", "needs", "reply_buttons", "steps_tick"];
33
34
  const MAX_HELD = 1000; // live events held during one catch-up read; more → read again afterwards
34
35
  const RETRY_DELAYS = [1000, 2000, 4000]; // after the first failed catch-up read; then it waits for Retry
35
36
  const POLL_LIMIT_MS = 10000; // a poll that has not answered by then counts as failed