@songsid/agend 2.2.0-beta.7 → 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 (72) hide show
  1. package/dist/agent-whats-new.js +9 -3
  2. package/dist/agent-whats-new.js.map +1 -1
  3. package/dist/backend/kiro.d.ts +9 -0
  4. package/dist/backend/kiro.js +23 -0
  5. package/dist/backend/kiro.js.map +1 -1
  6. package/dist/channel/mcp-tools.js +1 -1
  7. package/dist/channel/mcp-tools.js.map +1 -1
  8. package/dist/config-validator.js +3 -0
  9. package/dist/config-validator.js.map +1 -1
  10. package/dist/daemon.d.ts +2 -0
  11. package/dist/daemon.js +2 -0
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/fleet-manager.d.ts +12 -0
  14. package/dist/fleet-manager.js +45 -0
  15. package/dist/fleet-manager.js.map +1 -1
  16. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -0
  17. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  18. package/dist/instance-config-impact.js +1 -0
  19. package/dist/instance-config-impact.js.map +1 -1
  20. package/dist/log-service.d.ts +99 -0
  21. package/dist/log-service.js +235 -0
  22. package/dist/log-service.js.map +1 -0
  23. package/dist/log-steps.d.ts +143 -0
  24. package/dist/log-steps.js +646 -0
  25. package/dist/log-steps.js.map +1 -0
  26. package/dist/public-web-gateway.js +1 -1
  27. package/dist/public-web-gateway.js.map +1 -1
  28. package/dist/public-web-link.js +15 -3
  29. package/dist/public-web-link.js.map +1 -1
  30. package/dist/settings-api.js +4 -0
  31. package/dist/settings-api.js.map +1 -1
  32. package/dist/settings-change.js +2 -0
  33. package/dist/settings-change.js.map +1 -1
  34. package/dist/settings-effect.js +2 -0
  35. package/dist/settings-effect.js.map +1 -1
  36. package/dist/tool-progress.js +3 -2
  37. package/dist/tool-progress.js.map +1 -1
  38. package/dist/transcript-jsonl.d.ts +80 -3
  39. package/dist/transcript-jsonl.js +147 -10
  40. package/dist/transcript-jsonl.js.map +1 -1
  41. package/dist/transcript-monitor.d.ts +11 -0
  42. package/dist/transcript-monitor.js +61 -13
  43. package/dist/transcript-monitor.js.map +1 -1
  44. package/dist/transcript-sources.d.ts +24 -0
  45. package/dist/transcript-sources.js +205 -55
  46. package/dist/transcript-sources.js.map +1 -1
  47. package/dist/types.d.ts +6 -0
  48. package/dist/ui/panel-log.js +388 -0
  49. package/dist/ui/panel-settings.js +6 -1
  50. package/dist/ui/settings-strings.js +2 -0
  51. package/dist/ui/shared/app-i18n.js +2 -2
  52. package/dist/ui/shared/app-route.js +6 -1
  53. package/dist/ui/shared/app-shell.js +3 -3
  54. package/dist/ui/shared/app-stream.js +2 -1
  55. package/dist/ui/shared/app.css +40 -5
  56. package/dist/ui/shared/app.js +5 -0
  57. package/dist/ui/shared/fonts/README.md +1 -1
  58. package/dist/ui/shared/instance-nav.js +9 -6
  59. package/dist/ui/shared/instance-switch.js +4 -4
  60. package/dist/ui/shared/tokens.css +1 -1
  61. package/dist/web-api.d.ts +3 -0
  62. package/dist/web-api.js +41 -0
  63. package/dist/web-api.js.map +1 -1
  64. package/dist/web-auth.js +2 -1
  65. package/dist/web-auth.js.map +1 -1
  66. package/dist/web-settings-patch.d.ts +3 -1
  67. package/dist/web-settings-patch.js +4 -1
  68. package/dist/web-settings-patch.js.map +1 -1
  69. package/dist/web-shell-routes.d.ts +4 -0
  70. package/dist/web-shell-routes.js +9 -0
  71. package/dist/web-shell-routes.js.map +1 -1
  72. package/package.json +1 -1
@@ -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">
@@ -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
@@ -208,7 +208,7 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
208
208
  :root { --text-scale: 1; }
209
209
  :root[data-text-size="s"] { --text-scale: 0.9; }
210
210
  :root[data-text-size="l"] { --text-scale: 1.15; }
211
- .thread, .p-details .panel-body, .p-fleet .panel-body {
211
+ .thread, .p-details .panel-body, .p-fleet .panel-body, .p-log .panel-body {
212
212
  --fs-xs: calc(12px * var(--text-scale)); --lh-xs: calc(16px * var(--text-scale));
213
213
  --fs-sm: calc(13px * var(--text-scale)); --lh-sm: calc(18px * var(--text-scale));
214
214
  --fs-md: calc(15px * var(--text-scale)); --lh-md: calc(24px * var(--text-scale));
@@ -217,10 +217,40 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
217
217
  }
218
218
  /* #1523 N2: Details. */
219
219
  .p-details .d-tags { display: inline-flex; flex-wrap: wrap; gap: var(--s-1); }
220
+ /* #1218: an instance's Log — one column of steps, newest at the bottom; the filter bar above it. Classes only (#1300). */
221
+ .p-log .log-bar { display: flex; align-items: center; gap: var(--s-1); padding: var(--s-2) var(--s-4); border-bottom: 1px solid var(--border); flex: none; }
222
+ .p-log .log-bar .chip-btn.on { color: var(--text); background: var(--surface-2); font-weight: var(--fw-semibold); }
223
+ .p-log .log-live { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--text-3); }
224
+ .p-log .log-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-1); }
225
+ .p-log .log-item { padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface); min-width: 0; overflow-wrap: anywhere; }
226
+ .p-log .log-item.turn-start { margin-top: var(--s-4); }
227
+ .p-log .log-item.k-user { background: var(--user-bubble); }
228
+ .p-log .log-item.k-turn_end, .p-log .log-item.k-state, .p-log .log-item.k-event { background: transparent; color: var(--text-3); padding-block: 2px; font-size: var(--fs-sm); }
229
+ .p-log .log-item.k-error, .p-log .log-item.s-failed { border-left: 3px solid var(--danger); }
230
+ .p-log .log-head { display: flex; align-items: baseline; gap: var(--s-2); min-width: 0; }
231
+ .p-log .log-icon { flex: none; }
232
+ .p-log .log-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
233
+ .p-log .log-cat { font-weight: var(--fw-semibold); }
234
+ .p-log .log-subject { font-size: var(--fs-sm); }
235
+ .p-log .log-time { flex: none; font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
236
+ .p-log .log-status { flex: none; font-size: var(--fs-xs); color: var(--text-3); white-space: nowrap; }
237
+ .p-log .log-status.done { color: var(--ok); }
238
+ .p-log .log-status.failed { color: var(--danger); }
239
+ .p-log .log-status.interrupted { color: var(--warn, var(--text-3)); }
240
+ .p-log .log-status .spinner { display: inline-block; width: 8px; height: 8px; margin-right: 4px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }
241
+ .p-log .log-repeat { flex: none; font-size: var(--fs-xs); padding: 0 6px; border-radius: 8px; background: var(--surface-2); color: var(--text-2); }
242
+ .p-log .log-text { white-space: pre-wrap; margin-top: var(--s-1); font-size: var(--fs-sm); }
243
+ .p-log .log-text.mono { font-family: var(--font-mono); font-size: var(--fs-xs); }
244
+ .p-log .log-text.folded { max-height: 10.5em; overflow: hidden; mask-image: linear-gradient(to bottom, #000 70%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent); }
245
+ .p-log .link-btn { border: 0; background: none; padding: 2px 0; color: var(--accent); cursor: pointer; font-size: var(--fs-xs); }
246
+ .p-log .log-detail { margin-top: var(--s-1); display: flex; flex-direction: column; gap: var(--s-2); }
247
+ .p-log .log-sec-h { font-size: var(--fs-xs); color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
248
+ .p-log .log-older, .p-log .log-start { text-align: center; margin: var(--s-2) 0; }
249
+ .p-log .log-off { padding: var(--s-6) var(--s-4); }
220
250
  .p-details .d-edit { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); flex-wrap: wrap; margin-top: var(--s-2); }
221
251
  @media (max-width: 899px) {
222
252
  .panel-head:has(.inst-switch) { flex-wrap: wrap; }
223
- .inst-switch.seg { order: 3; flex-basis: 100%; display: grid; grid-template-columns: repeat(3, 1fr); padding: 0 0 var(--s-1); }
253
+ .inst-switch.seg { order: 3; flex-basis: 100%; display: grid; grid-template-columns: repeat(4, 1fr); padding: 0 0 var(--s-1); }
224
254
  .inst-switch .seg-item { justify-content: center; border-radius: var(--r-sm); }
225
255
  }
226
256
  .panel-body { flex: 1; min-height: 0; overflow-y: auto; }
@@ -655,11 +685,16 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
655
685
  .v-filter-clear { width: 28px; height: 28px; }
656
686
  .v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
657
687
  /* alpha.2 (N1): status and CLI chips under the filter box — each a group of checkboxes under its header (#1700). */
658
- .nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
659
- .nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
688
+ /* #1776: the "N / M" count is on the headers' row, at its right end — not a row of its own. The headers (and an open
689
+ group's list, the reset) wrap in their own box beside it, so a filter's summary or a narrow sidebar moves a header
690
+ down, never the count; the count is as tall as a header, so the row does not grow. */
691
+ .nav-facets { display: flex; align-items: flex-start; gap: var(--s-2); padding: var(--s-2) var(--s-1) 0; }
692
+ .nav-facet-groups { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); flex: 1 1 auto; min-width: 0; }
693
+ .nav-facets .nav-shown { flex: none; margin: 0 0 0 auto; padding: 2px 0; white-space: nowrap; }
694
+ .nav-facet { font-size: var(--fs-xs); color: var(--text-2); min-width: 0; max-width: 100%; }
660
695
  /* #1700: a facet group's header — the whole row toggles its list; folded with a filter set, a short summary. */
661
696
  .nav-facet-head { display: inline-flex; align-items: center; gap: var(--s-1); max-width: 100%; padding: 2px var(--s-2); border: 0; border-radius: var(--r-sm);
662
- background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; }
697
+ background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; white-space: nowrap; }
663
698
  .nav-facet-head:hover { background: var(--hover); }
664
699
  .nav-facet-head:focus-visible { outline: 2px solid var(--accent); }
665
700
  .nav-facet-head .caret { color: var(--text-3); flex: none; }
@@ -51,6 +51,8 @@ if (mode === "full") {
51
51
  stream.on("hydration", (hydration) => appStore.set({ hydration }));
52
52
  appStore.set({ retryHydration: () => stream.catchUp() });
53
53
  stream.on("needs", (d) => appStore.set({ needs: Array.isArray(d && d.items) ? d.items : [] }));
54
+ // #1218: a panel's way to hear one stream event (the Log's steps_tick), unsubscribed by its lease.
55
+ appStore.set({ streamOn: (name, fn) => stream.on(name, fn) });
54
56
  appStore.set({ connection: stream.connection() });
55
57
 
56
58
  // The chat's store must hear every message from the first frame, whatever panel is open: its module boots once,
@@ -67,6 +69,7 @@ if (mode === "full") {
67
69
  const loadSettings = retryable((a) => import(retryUrl("/ui/js/panel-settings.js", a)));
68
70
  const loadNeeds = retryable((a) => import(retryUrl("/ui/js/panel-needs.js", a)));
69
71
  const loadDetails = retryable((a) => import(retryUrl("/ui/js/panel-details.js", a)));
72
+ const loadLog = retryable((a) => import(retryUrl("/ui/js/panel-log.js", a)));
70
73
  // The stream opens once the chat listens, so the frames sent on connect (status, open prompts, ticks) reach it too.
71
74
  // If the chat cannot load (a session that just ended), the stream still opens for the sidebar.
72
75
  const chatBoot = loadChat();
@@ -80,6 +83,8 @@ if (mode === "full") {
80
83
  panels.set("needs", { load: () => loadNeeds().then((m) => m.NeedsPanel) });
81
84
  // #1523 N2: one instance's Details (/ui/fleet/agent/<name>).
82
85
  panels.set("details", { load: () => loadDetails().then((m) => m.DetailsPanel) });
86
+ // #1218: one instance's Log (/ui/log/<name>).
87
+ panels.set("log", { load: () => loadLog().then((m) => m.LogPanel) });
83
88
  // #1386 §6.3: desktop notifications for new "Needs you" items, in any panel, while this signed-in page is open.
84
89
  startNeedsNotifier({ open: (instance) => navigate(chatPath(instance)) });
85
90
  // #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
@@ -8,7 +8,7 @@
8
8
  - The optical-size axis is fixed at its text size (`opsz=14`).
9
9
  - The font is subset to Latin, with the same range as `unicode-range` in `tokens.css`.
10
10
  - It is saved as WOFF2. About 44 KB, against 352 KB for the full variable font.
11
- - Chinese and other scripts fall back to the system fonts named in `--font-ui` (PingFang TC → Noto Sans TC → Microsoft JhengHei → the system stack).
11
+ - Chinese and other scripts fall back to the system fonts named in `--font-ui` (PingFang TC → PingFang SC → Noto Sans TC → Noto Sans SC → Noto Sans CJK TC → Noto Sans CJK SC → Microsoft JhengHei → Microsoft YaHei → the system stack).
12
12
 
13
13
  Rebuilt with fontTools 4.59 (and `brotli` for WOFF2):
14
14
 
@@ -10,7 +10,7 @@
10
10
  import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
11
11
  import { t } from "./app-i18n.js";
12
12
  import { appStore, createStore, useStore } from "./app-store.js";
13
- import { chatPath, detailsPath, viewPath } from "./app-route.js";
13
+ import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
14
14
  import { Icon } from "./ui-icons.js";
15
15
  import { Skeleton } from "./ui-states.js";
16
16
  import { viewStore } from "./view-roster-store.js";
@@ -353,8 +353,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
353
353
  // Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
354
354
  // everywhere else → Chat. The anonymous reader only ever has View.
355
355
  const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
356
+ : route && route.panel === "log" ? logPath // #1218: Log → Log
356
357
  : route && (route.panel === "details" || route.panel === "fleet") ? detailsPath : chatPath;
357
- const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "details") ? route.instance : null;
358
+ const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
358
359
  useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
359
360
  const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
360
361
  const byName = new Map(items.map((it) => [nameOf(it), it]));
@@ -478,11 +479,13 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
478
479
  onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
479
480
  </label>
480
481
  <div class="nav-facets">
481
- ${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
482
- ${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
483
- ${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
482
+ <div class="nav-facet-groups">
483
+ ${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
484
+ ${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
485
+ ${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
486
+ </div>
487
+ <p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
484
488
  </div>
485
- <p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
486
489
  </div>
487
490
  </div>`;
488
491
  }
@@ -1,16 +1,16 @@
1
1
  // #1523 N2 (§3.2): the instance view switch — Chat | View | Details of ONE instance. Each segment is a link to that
2
2
  // view's URL for the same instance, so switching keeps the instance (and the sidebar, which is never remounted).
3
- // Desktop: links in the header; a phone: the same three as top tabs under it (app.css). The anonymous View reader
3
+ // Desktop: links in the header; a phone: the same four as top tabs under it (app.css). The anonymous View reader
4
4
  // gets none (Chat and Details need a session): the caller does not render it there.
5
5
  import { html } from "./app-html.js";
6
6
  import { t } from "./app-i18n.js";
7
- import { chatPath, detailsPath, viewPath } from "./app-route.js";
7
+ import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
8
8
 
9
- /** current: "chat" | "view" | "details". */
9
+ /** current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. */
10
10
  export function InstanceSwitch({ name, current }) {
11
11
  const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
12
12
  aria-current=${current === key ? "page" : undefined}>${label}</a>`;
13
13
  return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
14
- ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("details", detailsPath(name), t("app.details"))}
14
+ ${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
15
15
  </nav>`;
16
16
  }
@@ -22,7 +22,7 @@
22
22
 
23
23
  :root {
24
24
  /* ── Type ── */
25
- --font-ui: Inter, "PingFang TC", "Noto Sans TC", "Microsoft JhengHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
25
+ --font-ui: Inter, "PingFang TC", "PingFang SC", "Noto Sans TC", "Noto Sans SC", "Noto Sans CJK TC", "Noto Sans CJK SC", "Microsoft JhengHei", "Microsoft YaHei", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
26
  --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
27
27
  --fs-xs: 12px; --lh-xs: 16px;
28
28
  --fs-sm: 13px; --lh-sm: 18px;
package/dist/web-api.d.ts CHANGED
@@ -148,6 +148,8 @@ export interface WebApiContext {
148
148
  needsYouItems?(): unknown[];
149
149
  /** #1389: the org chart's structure (fleet.yaml's teams, General, descriptions, thread links); absent: an empty chart. */
150
150
  orgChart?(): unknown;
151
+ /** #1218: an instance's Log steps, read on demand (null: no such instance; `{ enabled: false }`: the Log is off). */
152
+ logSteps?(name: string, q: import("./log-service.js").LogQuery): Promise<unknown>;
151
153
  /** #1468: the prompt-cache expiry analysis for a window ("24h" | "7d" | "30d"); absent: not offered. */
152
154
  cacheReport?(window: string): Promise<unknown>;
153
155
  /** #1269: an instance's chat command from the web chat (web-commands.ts); absent: not offered. */
@@ -253,6 +255,7 @@ export interface AppShellContext {
253
255
  readonly fleetConfig?: {
254
256
  web?: {
255
257
  usage_panel?: boolean;
258
+ log_tab?: boolean;
256
259
  };
257
260
  } | null;
258
261
  /** #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
@@ -347,6 +347,45 @@ export function handleWebRequest(req, res, url, ctx) {
347
347
  json(res, 200, { messages: ctx.webChatHistory?.list(instance, limit) ?? [], boot: ctx.webChatHistory?.boot ?? null, lastId: ctx.webChatHistory?.lastId ?? 0 });
348
348
  return true;
349
349
  }
350
+ // ── #1218: the Log tab ─────────────────────────────────
351
+ // An instance's steps, read on demand from its own transcript (log-service.ts). Read-only, and offered on the public
352
+ // link on purpose (the user's decision): the same masking applies to every viewer. Passive (web-auth.ts).
353
+ if (method === "GET" && path === "/ui/steps") {
354
+ const p = url.searchParams;
355
+ const instance = p.get("instance") ?? "";
356
+ const num = (k) => {
357
+ const v = p.get(k);
358
+ if (v === null)
359
+ return undefined;
360
+ const n = Number(v);
361
+ return /^\d{1,15}$/.test(v) && Number.isSafeInteger(n) ? n : null;
362
+ };
363
+ const after = num("after"), before = num("before"), ev = num("ev"), limit = num("limit");
364
+ const key = p.get("key");
365
+ if (!instance || instance.length > 128 || after === null || before === null || ev === null || limit === null
366
+ || (limit !== undefined && (limit < 1 || limit > 200)) || (key !== null && !/^\d{1,20}-\d{1,20}$/.test(key))) {
367
+ json(res, 400, { error: "instance (1-128 chars); after, before, ev: offsets; limit 1-200; key: <dev>-<ino>" });
368
+ return true;
369
+ }
370
+ if (!ctx.logSteps) {
371
+ json(res, 404, { error: "not available" });
372
+ return true;
373
+ }
374
+ ctx.logSteps(instance, {
375
+ ...(after !== undefined ? { after } : {}), ...(before !== undefined ? { before } : {}), ...(ev !== undefined ? { ev } : {}),
376
+ ...(limit !== undefined ? { limit } : {}), ...(key !== null ? { key } : {}),
377
+ skip: p.get("skip") === "1", watch: p.get("watch") === "1",
378
+ }).then(answer => {
379
+ if (answer === null)
380
+ json(res, 404, { error: `Instance not found: ${instance}` });
381
+ else
382
+ json(res, 200, answer);
383
+ }).catch(err => {
384
+ ctx.logger.error({ err: err.message, instance }, "Log steps read failed");
385
+ json(res, 500, { error: "could not read the steps" });
386
+ });
387
+ return true;
388
+ }
350
389
  // ── Send message ───────────────────────────────────────
351
390
  if (method === "POST" && path === "/ui/send") {
352
391
  handleSendMessage(req, res, ctx);
@@ -1612,6 +1651,8 @@ function shellBodyTag(req, ctx, mode) {
1612
1651
  + `${p?.detail ? ` data-preview-detail="${attr(p.detail)}"` : ""}`
1613
1652
  // #1523 N3: whether the fleet serves /api/ai-usage (web.usage_panel), for every page's ◔.
1614
1653
  + ` data-usage-panel="${ctx.fleetConfig?.web?.usage_panel === false ? "0" : "1"}"`
1654
+ // #1218: whether the Log tab is on (web.log_tab; off by default): the tab says so and offers to turn it on.
1655
+ + ` data-log-tab="${ctx.fleetConfig?.web?.log_tab === true ? "1" : "0"}"`
1615
1656
  // #1770: the fleet's zone (chat-logs' and fleet.log's), in which the chat reads its message times and days.
1616
1657
  + ` data-fleet-tz="${attr(resolveTz())}"`
1617
1658
  // #1589: the public link shows attachments as downloads only (no inline text or media): its scope is unchanged.