@burdenoff/microfe-vibecontrols 2026.529.12 → 2026.529.13

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.
@@ -1,94 +1,93 @@
1
1
  import { useVibeControls as e } from "../../providers/VibeControlsProvider.js";
2
- import { agentApiPath as t } from "../../utils/agentApiPath.js";
3
- import { useCallback as n, useEffect as r, useRef as i, useState as a } from "react";
4
- import { useI18n as o } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
5
- import { Download as s, Pause as c, Play as l, RefreshCw as u, Search as d, Trash2 as f } from "lucide-react";
6
- import { getStoredAuthToken as p, getStoredWorkspaceToken as m } from "@burdenoff/fe-libs/shared/graphql";
7
- import { jsx as h, jsxs as g } from "react/jsx-runtime";
2
+ import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
3
+ import { useI18n as a } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
4
+ import { Download as o, Pause as s, Play as c, RefreshCw as l, Search as u, Trash2 as d } from "lucide-react";
5
+ import { getStoredAuthToken as f, getStoredWorkspaceToken as p } from "@burdenoff/fe-libs/shared/graphql";
6
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
8
7
  //#region src/components/logs/LogViewer.tsx
9
- function _(e) {
8
+ function g(e) {
10
9
  return `${e.timestamp}:${e.level}:${e.source}:${e.message}`;
11
10
  }
12
- var v = {
11
+ var _ = {
13
12
  debug: "text-text-tertiary",
14
13
  info: "text-status-info-text",
15
14
  warn: "text-status-warning-text",
16
15
  error: "text-status-error-text"
17
- }, y = {
16
+ }, v = {
18
17
  debug: "bg-bg-tertiary/30",
19
18
  info: "bg-status-info-bg/10",
20
19
  warn: "bg-status-warning-bg/10",
21
20
  error: "bg-status-error-bg/10"
22
- }, b = ({ agentId: b, profile: x, level: S = "info", source: C, maxEntries: w = 1e3, className: T = "", compact: E = !1 }) => {
23
- let { t: D } = o(), { apiGatewayUrl: O, workspaceId: k } = e(), [A, j] = a([]), [M, N] = a(!1), [P, F] = a(!1), [I, L] = a(S), [R, z] = a(""), [B, V] = a(null), [H, U] = a(!1), W = i(null), G = i(!0), K = (e, t) => {
24
- let n = D(e);
21
+ }, y = ({ agentId: y, profile: b, level: x = "info", source: S, maxEntries: C = 1e3, className: w = "", compact: T = !1 }) => {
22
+ let { t: E } = a(), { apiGatewayUrl: D, workspaceId: O } = e(), [k, A] = i([]), [j, M] = i(!1), [N, P] = i(!1), [F, I] = i(x), [L, R] = i(""), [z, B] = i(null), [V, H] = i(!1), U = r(null), W = r(!0), G = (e, t) => {
23
+ let n = E(e);
25
24
  return n === e ? t : n;
26
- }, q = n(() => {
27
- if (!W.current) return;
28
- let e = W.current;
29
- G.current = e.scrollHeight - e.scrollTop - e.clientHeight < 50;
30
- }, []), J = n(() => {
31
- W.current && (W.current.scrollTop = W.current.scrollHeight);
32
- }, []), Y = n(async () => {
33
- if (b) {
34
- U(!0);
25
+ }, K = t(() => {
26
+ if (!U.current) return;
27
+ let e = U.current;
28
+ W.current = e.scrollHeight - e.scrollTop - e.clientHeight < 50;
29
+ }, []), q = t(() => {
30
+ U.current && (U.current.scrollTop = U.current.scrollHeight);
31
+ }, []), J = t(async () => {
32
+ if (y) {
33
+ H(!0);
35
34
  try {
36
- let e = p(), n = m(), r = await (await fetch(O ? `${O}/workspaces/graphql` : "/workspaces/graphql", {
35
+ let e = f(), t = p(), n = await (await fetch(D ? `${D}/workspaces/graphql` : "/workspaces/graphql", {
37
36
  method: "POST",
38
37
  headers: {
39
38
  "Content-Type": "application/json",
40
39
  ...e ? { Authorization: `Bearer ${e}` } : {},
41
- ...n ? { "X-Workspace-Authorization": `Bearer ${n}` } : {},
42
- ...k ? { "x-workspace-id": k } : {}
40
+ ...t ? { "X-Workspace-Authorization": `Bearer ${t}` } : {},
41
+ ...O ? { "x-workspace-id": O } : {}
43
42
  },
44
43
  body: JSON.stringify({
45
- query: "query AgentLogs($agentId: ID!, $path: String!, $query: JSON) {\n agentAiGet(agentId: $agentId, path: $path, query: $query) {\n ok\n status\n data\n error\n }\n }",
44
+ query: "query AgentLogs($agentId: ID!, $profile: String, $query: JSON) {\n agentLogsGet(agentId: $agentId, profile: $profile, query: $query) {\n ok\n status\n data\n error\n }\n }",
46
45
  variables: {
47
- agentId: b,
48
- path: t(x, "/logs"),
46
+ agentId: y,
47
+ profile: b,
49
48
  query: {
50
- level: I,
49
+ level: F,
51
50
  limit: 200,
52
- ...C ? { source: C } : {}
51
+ ...S ? { source: S } : {}
53
52
  }
54
53
  }
55
54
  })
56
- })).json(), i = r.data?.agentAiGet;
57
- if (!i?.ok) throw Error(i?.error || r.errors?.[0]?.message || "Failed to fetch logs");
58
- let a = (i.data?.entries ?? []).slice().sort((e, t) => new Date(e.timestamp).getTime() - new Date(t.timestamp).getTime());
59
- j((e) => {
55
+ })).json(), r = n.data?.agentLogsGet;
56
+ if (!r?.ok) throw Error(r?.error || n.errors?.[0]?.message || "Failed to fetch logs");
57
+ let i = (r.data?.entries ?? []).slice().sort((e, t) => new Date(e.timestamp).getTime() - new Date(t.timestamp).getTime());
58
+ A((e) => {
60
59
  let t = /* @__PURE__ */ new Map();
61
- for (let n of e) t.set(_(n), n);
62
- for (let e of a) t.set(_(e), e);
63
- return Array.from(t.values()).sort((e, t) => new Date(e.timestamp).getTime() - new Date(t.timestamp).getTime()).slice(-w);
64
- }), N(!0), V(null), G.current && requestAnimationFrame(J);
60
+ for (let n of e) t.set(g(n), n);
61
+ for (let e of i) t.set(g(e), e);
62
+ return Array.from(t.values()).sort((e, t) => new Date(e.timestamp).getTime() - new Date(t.timestamp).getTime()).slice(-C);
63
+ }), M(!0), B(null), W.current && requestAnimationFrame(q);
65
64
  } catch (e) {
66
- N(!1), V(e instanceof Error ? e.message : K("logs.fetchFailed", "Failed to fetch logs"));
65
+ M(!1), B(e instanceof Error ? e.message : G("logs.fetchFailed", "Failed to fetch logs"));
67
66
  } finally {
68
- U(!1);
67
+ H(!1);
69
68
  }
70
69
  }
71
70
  }, [
71
+ y,
72
72
  b,
73
- x,
74
- O,
73
+ D,
74
+ S,
75
+ F,
75
76
  C,
76
- I,
77
- w,
78
- J,
79
- K,
80
- k
77
+ q,
78
+ G,
79
+ O
81
80
  ]);
82
- r(() => {
83
- j([]), N(!1), V(null);
81
+ n(() => {
82
+ A([]), M(!1), B(null);
84
83
  }, [
85
- b,
86
- C,
87
- I
88
- ]), r(() => {
89
- if (!b || P) return;
84
+ y,
85
+ S,
86
+ F
87
+ ]), n(() => {
88
+ if (!y || N) return;
90
89
  let e = !1, t = async () => {
91
- e || await Y();
90
+ e || await J();
92
91
  };
93
92
  t();
94
93
  let n = window.setInterval(() => {
@@ -98,14 +97,14 @@ var v = {
98
97
  e = !0, window.clearInterval(n);
99
98
  };
100
99
  }, [
101
- b,
102
- Y,
103
- P
100
+ y,
101
+ J,
102
+ N
104
103
  ]);
105
- let X = R ? A.filter((e) => e.message.toLowerCase().includes(R.toLowerCase()) || e.source.toLowerCase().includes(R.toLowerCase())) : A, Z = () => j([]), Q = () => {
106
- let e = new Blob([X.map((e) => JSON.stringify(e)).join("\n")], { type: "application/x-ndjson" }), t = URL.createObjectURL(e), n = document.createElement("a");
104
+ let Y = L ? k.filter((e) => e.message.toLowerCase().includes(L.toLowerCase()) || e.source.toLowerCase().includes(L.toLowerCase())) : k, X = () => A([]), Z = () => {
105
+ let e = new Blob([Y.map((e) => JSON.stringify(e)).join("\n")], { type: "application/x-ndjson" }), t = URL.createObjectURL(e), n = document.createElement("a");
107
106
  n.href = t, n.download = `agent-logs-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}.jsonl`, n.click(), URL.revokeObjectURL(t);
108
- }, $ = (e) => {
107
+ }, Q = (e) => {
109
108
  try {
110
109
  let t = new Date(e);
111
110
  return t.toLocaleTimeString("en-US", {
@@ -118,129 +117,129 @@ var v = {
118
117
  return e;
119
118
  }
120
119
  };
121
- return /* @__PURE__ */ g("div", {
122
- className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${T}`,
120
+ return /* @__PURE__ */ h("div", {
121
+ className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${w}`,
123
122
  children: [
124
- /* @__PURE__ */ g("div", {
123
+ /* @__PURE__ */ h("div", {
125
124
  className: "flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2",
126
125
  children: [
127
- /* @__PURE__ */ g("div", {
126
+ /* @__PURE__ */ h("div", {
128
127
  className: "mr-2 flex items-center gap-1.5",
129
- children: [/* @__PURE__ */ h("div", { className: `size-2 rounded-full ${M ? "animate-pulse bg-status-success-bg" : "bg-status-error-bg"}` }), /* @__PURE__ */ h("span", {
128
+ children: [/* @__PURE__ */ m("div", { className: `size-2 rounded-full ${j ? "animate-pulse bg-status-success-bg" : "bg-status-error-bg"}` }), /* @__PURE__ */ m("span", {
130
129
  className: "text-xs text-text-tertiary",
131
- children: M ? K("logs.live", "Live") : K("logs.disconnected", "Disconnected")
130
+ children: j ? G("logs.live", "Live") : G("logs.disconnected", "Disconnected")
132
131
  })]
133
132
  }),
134
- /* @__PURE__ */ g("select", {
135
- value: I,
136
- onChange: (e) => L(e.target.value),
133
+ /* @__PURE__ */ h("select", {
134
+ value: F,
135
+ onChange: (e) => I(e.target.value),
137
136
  className: "rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary",
138
- "aria-label": K("logs.levelFilter", "Log level filter"),
137
+ "aria-label": G("logs.levelFilter", "Log level filter"),
139
138
  children: [
140
- /* @__PURE__ */ h("option", {
139
+ /* @__PURE__ */ m("option", {
141
140
  value: "debug",
142
141
  children: "DEBUG+"
143
142
  }),
144
- /* @__PURE__ */ h("option", {
143
+ /* @__PURE__ */ m("option", {
145
144
  value: "info",
146
145
  children: "INFO+"
147
146
  }),
148
- /* @__PURE__ */ h("option", {
147
+ /* @__PURE__ */ m("option", {
149
148
  value: "warn",
150
149
  children: "WARN+"
151
150
  }),
152
- /* @__PURE__ */ h("option", {
151
+ /* @__PURE__ */ m("option", {
153
152
  value: "error",
154
153
  children: "ERROR"
155
154
  })
156
155
  ]
157
156
  }),
158
- /* @__PURE__ */ g("div", {
157
+ /* @__PURE__ */ h("div", {
159
158
  className: "relative min-w-[120px] max-w-xs flex-1",
160
- children: [/* @__PURE__ */ h(d, {
159
+ children: [/* @__PURE__ */ m(u, {
161
160
  size: 12,
162
161
  className: "absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary"
163
- }), /* @__PURE__ */ h("input", {
162
+ }), /* @__PURE__ */ m("input", {
164
163
  "aria-label": "control",
165
164
  type: "text",
166
- placeholder: K("logs.filterLogs", "Filter logs..."),
167
- value: R,
168
- onChange: (e) => z(e.target.value),
165
+ placeholder: G("logs.filterLogs", "Filter logs..."),
166
+ value: L,
167
+ onChange: (e) => R(e.target.value),
169
168
  className: "w-full rounded border border-border-secondary bg-bg-tertiary py-1 pl-6 pr-2 text-xs text-text-primary placeholder:text-text-tertiary"
170
169
  })]
171
170
  }),
172
- /* @__PURE__ */ g("div", {
171
+ /* @__PURE__ */ h("div", {
173
172
  className: "ml-auto flex items-center gap-1",
174
173
  children: [
175
- /* @__PURE__ */ g("span", {
174
+ /* @__PURE__ */ h("span", {
176
175
  className: "text-xs text-text-tertiary",
177
176
  children: [
178
- X.length,
177
+ Y.length,
179
178
  " ",
180
- K("logs.entries", "entries")
179
+ G("logs.entries", "entries")
181
180
  ]
182
181
  }),
183
- /* @__PURE__ */ h("button", {
182
+ /* @__PURE__ */ m("button", {
184
183
  type: "button",
185
- onClick: () => void Y(),
186
- disabled: H,
184
+ onClick: () => void J(),
185
+ disabled: V,
187
186
  className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50",
188
- title: K("common.refresh", "Refresh"),
189
- children: /* @__PURE__ */ h(u, {
187
+ title: G("common.refresh", "Refresh"),
188
+ children: /* @__PURE__ */ m(l, {
190
189
  size: 14,
191
- className: H ? "animate-spin" : ""
190
+ className: V ? "animate-spin" : ""
192
191
  })
193
192
  }),
194
- /* @__PURE__ */ h("button", {
193
+ /* @__PURE__ */ m("button", {
195
194
  type: "button",
196
- onClick: () => F((e) => !e),
197
- className: `rounded p-1 transition-colors hover:bg-bg-tertiary ${P ? "text-status-warning-text" : "text-text-tertiary"}`,
198
- title: P ? K("logs.resume", "Resume") : K("logs.pause", "Pause"),
199
- children: h(P ? l : c, { size: 14 })
195
+ onClick: () => P((e) => !e),
196
+ className: `rounded p-1 transition-colors hover:bg-bg-tertiary ${N ? "text-status-warning-text" : "text-text-tertiary"}`,
197
+ title: N ? G("logs.resume", "Resume") : G("logs.pause", "Pause"),
198
+ children: m(N ? c : s, { size: 14 })
200
199
  }),
201
- /* @__PURE__ */ h("button", {
200
+ /* @__PURE__ */ m("button", {
202
201
  type: "button",
203
- onClick: Z,
202
+ onClick: X,
204
203
  className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary",
205
- title: K("logs.clear", "Clear"),
206
- children: /* @__PURE__ */ h(f, { size: 14 })
204
+ title: G("logs.clear", "Clear"),
205
+ children: /* @__PURE__ */ m(d, { size: 14 })
207
206
  }),
208
- /* @__PURE__ */ h("button", {
207
+ /* @__PURE__ */ m("button", {
209
208
  type: "button",
210
- onClick: Q,
209
+ onClick: Z,
211
210
  className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary",
212
- title: K("logs.export", "Export"),
213
- children: /* @__PURE__ */ h(s, { size: 14 })
211
+ title: G("logs.export", "Export"),
212
+ children: /* @__PURE__ */ m(o, { size: 14 })
214
213
  })
215
214
  ]
216
215
  })
217
216
  ]
218
217
  }),
219
- B ? /* @__PURE__ */ h("div", {
218
+ z ? /* @__PURE__ */ m("div", {
220
219
  role: "alert",
221
220
  className: "border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text",
222
- children: B
221
+ children: z
223
222
  }) : null,
224
- /* @__PURE__ */ h("div", {
225
- ref: W,
226
- onScroll: q,
227
- className: `overflow-y-auto font-mono text-xs leading-relaxed ${E ? "max-h-[300px]" : "min-h-[400px] flex-1"}`,
228
- style: { maxHeight: E ? "300px" : void 0 },
229
- children: X.length === 0 ? /* @__PURE__ */ h("div", {
223
+ /* @__PURE__ */ m("div", {
224
+ ref: U,
225
+ onScroll: K,
226
+ className: `overflow-y-auto font-mono text-xs leading-relaxed ${T ? "max-h-[300px]" : "min-h-[400px] flex-1"}`,
227
+ style: { maxHeight: T ? "300px" : void 0 },
228
+ children: Y.length === 0 ? /* @__PURE__ */ m("div", {
230
229
  className: "flex h-full min-h-[200px] items-center justify-center text-text-tertiary",
231
- children: M ? K("logs.waitingForEntries", "Waiting for log entries...") : K("logs.connectToAgent", "Connect to an agent to view logs")
232
- }) : X.map((e, t) => /* @__PURE__ */ g("div", {
233
- className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${y[e.level] ?? ""}`,
230
+ children: j ? G("logs.waitingForEntries", "Waiting for log entries...") : G("logs.connectToAgent", "Connect to an agent to view logs")
231
+ }) : Y.map((e, t) => /* @__PURE__ */ h("div", {
232
+ className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${v[e.level] ?? ""}`,
234
233
  children: [
235
- /* @__PURE__ */ h("span", {
234
+ /* @__PURE__ */ m("span", {
236
235
  className: "w-[85px] shrink-0 text-text-tertiary",
237
- children: $(e.timestamp)
236
+ children: Q(e.timestamp)
238
237
  }),
239
- /* @__PURE__ */ h("span", {
240
- className: `w-[42px] shrink-0 uppercase font-semibold ${v[e.level] ?? "text-text-primary"}`,
238
+ /* @__PURE__ */ m("span", {
239
+ className: `w-[42px] shrink-0 uppercase font-semibold ${_[e.level] ?? "text-text-primary"}`,
241
240
  children: e.level
242
241
  }),
243
- /* @__PURE__ */ g("span", {
242
+ /* @__PURE__ */ h("span", {
244
243
  className: "w-[100px] shrink-0 truncate text-text-secondary",
245
244
  title: e.source,
246
245
  children: [
@@ -249,9 +248,9 @@ var v = {
249
248
  "]"
250
249
  ]
251
250
  }),
252
- /* @__PURE__ */ g("span", {
251
+ /* @__PURE__ */ h("span", {
253
252
  className: "flex-1 break-all text-text-primary",
254
- children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */ h("span", {
253
+ children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */ m("span", {
255
254
  className: "ml-2 text-text-tertiary",
256
255
  children: JSON.stringify(e.metadata)
257
256
  }) : null]
@@ -263,6 +262,6 @@ var v = {
263
262
  });
264
263
  };
265
264
  //#endregion
266
- export { b as LogViewer };
265
+ export { y as LogViewer };
267
266
 
268
267
  //# sourceMappingURL=LogViewer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"LogViewer.js","names":[],"sources":["../../../src/components/logs/LogViewer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type FC } from 'react';\nimport { Download, Pause, Play, RefreshCw, Search, Trash2 } from 'lucide-react';\nimport { getStoredAuthToken, getStoredWorkspaceToken } from '@burdenoff/fe-libs/shared/graphql';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\ninterface LogEntry {\n timestamp: string;\n level: 'debug' | 'info' | 'warn' | 'error';\n source: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface LogViewerProps {\n agentId: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n level?: 'debug' | 'info' | 'warn' | 'error';\n source?: string;\n maxEntries?: number;\n className?: string;\n compact?: boolean;\n}\n\nfunction getEntryKey(entry: LogEntry): string {\n return `${entry.timestamp}:${entry.level}:${entry.source}:${entry.message}`;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: 'text-text-tertiary',\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n debug: 'bg-bg-tertiary/30',\n info: 'bg-status-info-bg/10',\n warn: 'bg-status-warning-bg/10',\n error: 'bg-status-error-bg/10',\n};\n\nexport const LogViewer: FC<LogViewerProps> = ({\n agentId,\n profile,\n level: initialLevel = 'info',\n source: initialSource,\n maxEntries = 1000,\n className = '',\n compact = false,\n}) => {\n const { t } = useI18n();\n const { apiGatewayUrl, workspaceId } = useVibeControls();\n const [entries, setEntries] = useState<LogEntry[]>([]);\n const [connected, setConnected] = useState(false);\n const [paused, setPaused] = useState(false);\n const [level, setLevel] = useState(initialLevel);\n const [searchQuery, setSearchQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const [isRefreshing, setIsRefreshing] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const autoScrollRef = useRef(true);\n\n const tr = (key: string, fallback: string) => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleScroll = useCallback(() => {\n if (!containerRef.current) {\n return;\n }\n\n const element = containerRef.current;\n autoScrollRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 50;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n }, []);\n\n const fetchLogs = useCallback(async () => {\n if (!agentId) {\n return;\n }\n\n setIsRefreshing(true);\n\n try {\n const authToken = getStoredAuthToken();\n const workspaceToken = getStoredWorkspaceToken();\n const response = await fetch(\n apiGatewayUrl ? `${apiGatewayUrl}/workspaces/graphql` : '/workspaces/graphql',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),\n ...(workspaceToken ? { 'X-Workspace-Authorization': `Bearer ${workspaceToken}` } : {}),\n ...(workspaceId ? { 'x-workspace-id': workspaceId } : {}),\n },\n body: JSON.stringify({\n query: `query AgentLogs($agentId: ID!, $path: String!, $query: JSON) {\n agentAiGet(agentId: $agentId, path: $path, query: $query) {\n ok\n status\n data\n error\n }\n }`,\n variables: {\n agentId,\n path: agentApiPath(profile, '/logs'),\n query: {\n level,\n limit: 200,\n ...(initialSource ? { source: initialSource } : {}),\n },\n },\n }),\n }\n );\n\n const payload = await response.json();\n const proxy = payload.data?.agentAiGet;\n if (!proxy?.ok) {\n throw new Error(proxy?.error || payload.errors?.[0]?.message || 'Failed to fetch logs');\n }\n\n const nextEntries = ((proxy.data?.entries ?? []) as LogEntry[]).slice().sort((a, b) => {\n return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();\n });\n setEntries((current) => {\n const merged = new Map<string, LogEntry>();\n for (const entry of current) {\n merged.set(getEntryKey(entry), entry);\n }\n for (const entry of nextEntries) {\n merged.set(getEntryKey(entry), entry);\n }\n return Array.from(merged.values())\n .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())\n .slice(-maxEntries);\n });\n setConnected(true);\n setErrorMessage(null);\n\n if (autoScrollRef.current) {\n requestAnimationFrame(scrollToBottom);\n }\n } catch (error) {\n setConnected(false);\n setErrorMessage(\n error instanceof Error ? error.message : tr('logs.fetchFailed', 'Failed to fetch logs')\n );\n } finally {\n setIsRefreshing(false);\n }\n }, [\n agentId,\n profile,\n apiGatewayUrl,\n initialSource,\n level,\n maxEntries,\n scrollToBottom,\n tr,\n workspaceId,\n ]);\n\n useEffect(() => {\n setEntries([]);\n setConnected(false);\n setErrorMessage(null);\n }, [agentId, initialSource, level]);\n\n useEffect(() => {\n if (!agentId || paused) {\n return undefined;\n }\n\n let cancelled = false;\n const load = async () => {\n if (!cancelled) {\n await fetchLogs();\n }\n };\n\n void load();\n const intervalId = window.setInterval(() => {\n void load();\n }, 3000);\n\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [agentId, fetchLogs, paused]);\n\n const filteredEntries = searchQuery\n ? entries.filter(\n (entry) =>\n entry.message.toLowerCase().includes(searchQuery.toLowerCase()) ||\n entry.source.toLowerCase().includes(searchQuery.toLowerCase())\n )\n : entries;\n\n const handleClear = () => setEntries([]);\n\n const handleExport = () => {\n const blob = new Blob([filteredEntries.map((entry) => JSON.stringify(entry)).join('\\n')], {\n type: 'application/x-ndjson',\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `agent-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.jsonl`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n const formatTime = (timestamp: string) => {\n try {\n const date = new Date(timestamp);\n return (\n date.toLocaleTimeString('en-US', {\n hour12: false,\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }) +\n '.' +\n String(date.getMilliseconds()).padStart(3, '0')\n );\n } catch {\n return timestamp;\n }\n };\n\n return (\n <div\n className={`flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${className}`}\n >\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2\">\n <div className=\"mr-2 flex items-center gap-1.5\">\n <div\n className={`size-2 rounded-full ${connected ? 'animate-pulse bg-status-success-bg' : 'bg-status-error-bg'}`}\n />\n <span className=\"text-xs text-text-tertiary\">\n {connected ? tr('logs.live', 'Live') : tr('logs.disconnected', 'Disconnected')}\n </span>\n </div>\n\n <select\n value={level}\n onChange={(event) => setLevel(event.target.value as LogEntry['level'])}\n className=\"rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary\"\n aria-label={tr('logs.levelFilter', 'Log level filter')}\n >\n <option value=\"debug\">DEBUG+</option>\n <option value=\"info\">INFO+</option>\n <option value=\"warn\">WARN+</option>\n <option value=\"error\">ERROR</option>\n </select>\n\n <div className=\"relative min-w-[120px] max-w-xs flex-1\">\n <Search\n size={12}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary\"\n />\n <input\n aria-label=\"control\"\n type=\"text\"\n placeholder={tr('logs.filterLogs', 'Filter logs...')}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full rounded border border-border-secondary bg-bg-tertiary py-1 pl-6 pr-2 text-xs text-text-primary placeholder:text-text-tertiary\"\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1\">\n <span className=\"text-xs text-text-tertiary\">\n {filteredEntries.length} {tr('logs.entries', 'entries')}\n </span>\n <button\n type=\"button\"\n onClick={() => void fetchLogs()}\n disabled={isRefreshing}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />\n </button>\n <button\n type=\"button\"\n onClick={() => setPaused((current) => !current)}\n className={`rounded p-1 transition-colors hover:bg-bg-tertiary ${paused ? 'text-status-warning-text' : 'text-text-tertiary'}`}\n title={paused ? tr('logs.resume', 'Resume') : tr('logs.pause', 'Pause')}\n >\n {paused ? <Play size={14} /> : <Pause size={14} />}\n </button>\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.clear', 'Clear')}\n >\n <Trash2 size={14} />\n </button>\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.export', 'Export')}\n >\n <Download size={14} />\n </button>\n </div>\n </div>\n\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text\"\n >\n {errorMessage}\n </div>\n ) : null}\n\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className={`overflow-y-auto font-mono text-xs leading-relaxed ${compact ? 'max-h-[300px]' : 'min-h-[400px] flex-1'}`}\n style={{ maxHeight: compact ? '300px' : undefined }}\n >\n {filteredEntries.length === 0 ? (\n <div className=\"flex h-full min-h-[200px] items-center justify-center text-text-tertiary\">\n {connected\n ? tr('logs.waitingForEntries', 'Waiting for log entries...')\n : tr('logs.connectToAgent', 'Connect to an agent to view logs')}\n </div>\n ) : (\n filteredEntries.map((entry, index) => (\n <div\n key={`${entry.timestamp}-${entry.source}-${index}`}\n className={`flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${LEVEL_BG[entry.level] ?? ''}`}\n >\n <span className=\"w-[85px] shrink-0 text-text-tertiary\">\n {formatTime(entry.timestamp)}\n </span>\n <span\n className={`w-[42px] shrink-0 uppercase font-semibold ${LEVEL_COLORS[entry.level] ?? 'text-text-primary'}`}\n >\n {entry.level}\n </span>\n <span\n className=\"w-[100px] shrink-0 truncate text-text-secondary\"\n title={entry.source}\n >\n [{entry.source}]\n </span>\n <span className=\"flex-1 break-all text-text-primary\">\n {entry.message}\n {entry.metadata && Object.keys(entry.metadata).length > 0 ? (\n <span className=\"ml-2 text-text-tertiary\">{JSON.stringify(entry.metadata)}</span>\n ) : null}\n </span>\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;AA0BA,SAAS,EAAY,GAAyB;AAC5C,QAAO,GAAG,EAAM,UAAU,GAAG,EAAM,MAAM,GAAG,EAAM,OAAO,GAAG,EAAM;;AAGpE,IAAM,IAAuC;CAC3C,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR,EAEK,IAAmC;CACvC,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR,EAEY,KAAiC,EAC5C,YACA,YACA,OAAO,IAAe,QACtB,QAAQ,GACR,gBAAa,KACb,eAAY,IACZ,aAAU,SACN;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,EAAE,kBAAe,mBAAgB,GAAiB,EAClD,CAAC,GAAS,KAAc,EAAqB,EAAE,CAAC,EAChD,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAQ,KAAa,EAAS,GAAM,EACrC,CAAC,GAAO,KAAY,EAAS,EAAa,EAC1C,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAc,KAAmB,EAAwB,KAAK,EAC/D,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,IAAe,EAAuB,KAAK,EAC3C,IAAgB,EAAO,GAAK,EAE5B,KAAM,GAAa,MAAqB;EAC5C,IAAM,IAAQ,EAAE,EAAI;AACpB,SAAO,MAAU,IAAM,IAAW;IAG9B,IAAe,QAAkB;AACrC,MAAI,CAAC,EAAa,QAChB;EAGF,IAAM,IAAU,EAAa;AAC7B,IAAc,UAAU,EAAQ,eAAe,EAAQ,YAAY,EAAQ,eAAe;IACzF,EAAE,CAAC,EAEA,IAAiB,QAAkB;AACvC,EAAI,EAAa,YACf,EAAa,QAAQ,YAAY,EAAa,QAAQ;IAEvD,EAAE,CAAC,EAEA,IAAY,EAAY,YAAY;AACnC,SAIL;KAAgB,GAAK;AAErB,OAAI;IACF,IAAM,IAAY,GAAoB,EAChC,IAAiB,GAAyB,EAiC1C,IAAU,OAhCC,MAAM,MACrB,IAAgB,GAAG,EAAc,uBAAuB,uBACxD;KACE,QAAQ;KACR,SAAS;MACP,gBAAgB;MAChB,GAAI,IAAY,EAAE,eAAe,UAAU,KAAa,GAAG,EAAE;MAC7D,GAAI,IAAiB,EAAE,6BAA6B,UAAU,KAAkB,GAAG,EAAE;MACrF,GAAI,IAAc,EAAE,kBAAkB,GAAa,GAAG,EAAE;MACzD;KACD,MAAM,KAAK,UAAU;MACnB,OAAO;MAQP,WAAW;OACT;OACA,MAAM,EAAa,GAAS,QAAQ;OACpC,OAAO;QACL;QACA,OAAO;QACP,GAAI,IAAgB,EAAE,QAAQ,GAAe,GAAG,EAAE;QACnD;OACF;MACF,CAAC;KACH,CACF,EAE8B,MAAM,EAC/B,IAAQ,EAAQ,MAAM;AAC5B,QAAI,CAAC,GAAO,GACV,OAAU,MAAM,GAAO,SAAS,EAAQ,SAAS,IAAI,WAAW,uBAAuB;IAGzF,IAAM,KAAgB,EAAM,MAAM,WAAW,EAAE,EAAiB,OAAO,CAAC,MAAM,GAAG,MACxE,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CACxE;AAgBF,IAfA,GAAY,MAAY;KACtB,IAAM,oBAAS,IAAI,KAAuB;AAC1C,UAAK,IAAM,KAAS,EAClB,GAAO,IAAI,EAAY,EAAM,EAAE,EAAM;AAEvC,UAAK,IAAM,KAAS,EAClB,GAAO,IAAI,EAAY,EAAM,EAAE,EAAM;AAEvC,YAAO,MAAM,KAAK,EAAO,QAAQ,CAAC,CAC/B,MAAM,GAAG,MAAM,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,GAAG,IAAI,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,CACjF,MAAM,CAAC,EAAW;MACrB,EACF,EAAa,GAAK,EAClB,EAAgB,KAAK,EAEjB,EAAc,WAChB,sBAAsB,EAAe;YAEhC,GAAO;AAEd,IADA,EAAa,GAAM,EACnB,EACE,aAAiB,QAAQ,EAAM,UAAU,EAAG,oBAAoB,uBAAuB,CACxF;aACO;AACR,MAAgB,GAAM;;;IAEvB;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CAAC;AAQF,CANA,QAAgB;AAGd,EAFA,EAAW,EAAE,CAAC,EACd,EAAa,GAAM,EACnB,EAAgB,KAAK;IACpB;EAAC;EAAS;EAAe;EAAM,CAAC,EAEnC,QAAgB;AACd,MAAI,CAAC,KAAW,EACd;EAGF,IAAI,IAAY,IACV,IAAO,YAAY;AACvB,GAAK,KACH,MAAM,GAAW;;AAIhB,KAAM;EACX,IAAM,IAAa,OAAO,kBAAkB;AACrC,MAAM;KACV,IAAK;AAER,eAAa;AAEX,GADA,IAAY,IACZ,OAAO,cAAc,EAAW;;IAEjC;EAAC;EAAS;EAAW;EAAO,CAAC;CAEhC,IAAM,IAAkB,IACpB,EAAQ,QACL,MACC,EAAM,QAAQ,aAAa,CAAC,SAAS,EAAY,aAAa,CAAC,IAC/D,EAAM,OAAO,aAAa,CAAC,SAAS,EAAY,aAAa,CAAC,CACjE,GACD,GAEE,UAAoB,EAAW,EAAE,CAAC,EAElC,UAAqB;EACzB,IAAM,IAAO,IAAI,KAAK,CAAC,EAAgB,KAAK,MAAU,KAAK,UAAU,EAAM,CAAC,CAAC,KAAK,KAAK,CAAC,EAAE,EACxF,MAAM,wBACP,CAAC,EACI,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAI1C,EAHA,EAAO,OAAO,GACd,EAAO,WAAW,+BAAc,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,MAAM,IAAI,CAAC,SACzF,EAAO,OAAO,EACd,IAAI,gBAAgB,EAAI;IAGpB,KAAc,MAAsB;AACxC,MAAI;GACF,IAAM,IAAO,IAAI,KAAK,EAAU;AAChC,UACE,EAAK,mBAAmB,SAAS;IAC/B,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,QAAQ;IACT,CAAC,GACF,MACA,OAAO,EAAK,iBAAiB,CAAC,CAAC,SAAS,GAAG,IAAI;UAE3C;AACN,UAAO;;;AAIX,QACE,kBAAC,OAAD;EACE,WAAW,wFAAwF;YADrG;GAGE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EACE,WAAW,uBAAuB,IAAY,uCAAuC,wBACrF,CAAA,EACF,kBAAC,QAAD;OAAM,WAAU;iBACb,IAAY,EAAG,aAAa,OAAO,GAAG,EAAG,qBAAqB,eAAe;OACzE,CAAA,CACH;;KAEN,kBAAC,UAAD;MACE,OAAO;MACP,WAAW,MAAU,EAAS,EAAM,OAAO,MAA2B;MACtE,WAAU;MACV,cAAY,EAAG,oBAAoB,mBAAmB;gBAJxD;OAME,kBAAC,UAAD;QAAQ,OAAM;kBAAQ;QAAe,CAAA;OACrC,kBAAC,UAAD;QAAQ,OAAM;kBAAO;QAAc,CAAA;OACnC,kBAAC,UAAD;QAAQ,OAAM;kBAAO;QAAc,CAAA;OACnC,kBAAC,UAAD;QAAQ,OAAM;kBAAQ;QAAc,CAAA;OAC7B;;KAET,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,MAAM;OACN,WAAU;OACV,CAAA,EACF,kBAAC,SAAD;OACE,cAAW;OACX,MAAK;OACL,aAAa,EAAG,mBAAmB,iBAAiB;OACpD,OAAO;OACP,WAAW,MAAU,EAAe,EAAM,OAAO,MAAM;OACvD,WAAU;OACV,CAAA,CACE;;KAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAgB;SAAO;SAAE,EAAG,gBAAgB,UAAU;SAClD;;OACP,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,KAAK,GAAW;QAC/B,UAAU;QACV,WAAU;QACV,OAAO,EAAG,kBAAkB,UAAU;kBAEtC,kBAAC,GAAD;SAAW,MAAM;SAAI,WAAW,IAAe,iBAAiB;SAAM,CAAA;QAC/D,CAAA;OACT,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,GAAW,MAAY,CAAC,EAAQ;QAC/C,WAAW,sDAAsD,IAAS,6BAA6B;QACvG,OAAO,IAAS,EAAG,eAAe,SAAS,GAAG,EAAG,cAAc,QAAQ;kBAE7D,EAAT,IAAU,IAAqB,GAAtB,EAAM,MAAM,IAAM,CAAsB;QAC3C,CAAA;OACT,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;QACV,OAAO,EAAG,cAAc,QAAQ;kBAEhC,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA;QACb,CAAA;OACT,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;QACV,OAAO,EAAG,eAAe,SAAS;kBAElC,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;QACf,CAAA;OACL;;KACF;;GAEL,IACC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;cAET;IACG,CAAA,GACJ;GAEJ,kBAAC,OAAD;IACE,KAAK;IACL,UAAU;IACV,WAAW,qDAAqD,IAAU,kBAAkB;IAC5F,OAAO,EAAE,WAAW,IAAU,UAAU,KAAA,GAAW;cAElD,EAAgB,WAAW,IAC1B,kBAAC,OAAD;KAAK,WAAU;eACZ,IACG,EAAG,0BAA0B,6BAA6B,GAC1D,EAAG,uBAAuB,mCAAmC;KAC7D,CAAA,GAEN,EAAgB,KAAK,GAAO,MAC1B,kBAAC,OAAD;KAEE,WAAW,oFAAoF,EAAS,EAAM,UAAU;eAF1H;MAIE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAW,EAAM,UAAU;OACvB,CAAA;MACP,kBAAC,QAAD;OACE,WAAW,6CAA6C,EAAa,EAAM,UAAU;iBAEpF,EAAM;OACF,CAAA;MACP,kBAAC,QAAD;OACE,WAAU;OACV,OAAO,EAAM;iBAFf;QAGC;QACG,EAAM;QAAO;QACV;;MACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CACG,EAAM,SACN,EAAM,YAAY,OAAO,KAAK,EAAM,SAAS,CAAC,SAAS,IACtD,kBAAC,QAAD;QAAM,WAAU;kBAA2B,KAAK,UAAU,EAAM,SAAS;QAAQ,CAAA,GAC/E,KACC;;MACH;OAvBC,GAAG,EAAM,UAAU,GAAG,EAAM,OAAO,GAAG,IAuBvC,CACN;IAEA,CAAA;GACF"}
1
+ {"version":3,"file":"LogViewer.js","names":[],"sources":["../../../src/components/logs/LogViewer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type FC } from 'react';\nimport { Download, Pause, Play, RefreshCw, Search, Trash2 } from 'lucide-react';\nimport { getStoredAuthToken, getStoredWorkspaceToken } from '@burdenoff/fe-libs/shared/graphql';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\ninterface LogEntry {\n timestamp: string;\n level: 'debug' | 'info' | 'warn' | 'error';\n source: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface LogViewerProps {\n agentId: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n level?: 'debug' | 'info' | 'warn' | 'error';\n source?: string;\n maxEntries?: number;\n className?: string;\n compact?: boolean;\n}\n\nfunction getEntryKey(entry: LogEntry): string {\n return `${entry.timestamp}:${entry.level}:${entry.source}:${entry.message}`;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: 'text-text-tertiary',\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n debug: 'bg-bg-tertiary/30',\n info: 'bg-status-info-bg/10',\n warn: 'bg-status-warning-bg/10',\n error: 'bg-status-error-bg/10',\n};\n\nexport const LogViewer: FC<LogViewerProps> = ({\n agentId,\n profile,\n level: initialLevel = 'info',\n source: initialSource,\n maxEntries = 1000,\n className = '',\n compact = false,\n}) => {\n const { t } = useI18n();\n const { apiGatewayUrl, workspaceId } = useVibeControls();\n const [entries, setEntries] = useState<LogEntry[]>([]);\n const [connected, setConnected] = useState(false);\n const [paused, setPaused] = useState(false);\n const [level, setLevel] = useState(initialLevel);\n const [searchQuery, setSearchQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const [isRefreshing, setIsRefreshing] = useState(false);\n const containerRef = useRef<HTMLDivElement>(null);\n const autoScrollRef = useRef(true);\n\n const tr = (key: string, fallback: string) => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleScroll = useCallback(() => {\n if (!containerRef.current) {\n return;\n }\n\n const element = containerRef.current;\n autoScrollRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 50;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n }, []);\n\n const fetchLogs = useCallback(async () => {\n if (!agentId) {\n return;\n }\n\n setIsRefreshing(true);\n\n try {\n const authToken = getStoredAuthToken();\n const workspaceToken = getStoredWorkspaceToken();\n const response = await fetch(\n apiGatewayUrl ? `${apiGatewayUrl}/workspaces/graphql` : '/workspaces/graphql',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),\n ...(workspaceToken ? { 'X-Workspace-Authorization': `Bearer ${workspaceToken}` } : {}),\n ...(workspaceId ? { 'x-workspace-id': workspaceId } : {}),\n },\n body: JSON.stringify({\n // Dedicated logs proxy (svc commit \"logs proxy: agentLogsGet\").\n // The previous path threaded `/api/profiles/<p>/logs` through\n // agentAiGet whose F9 allowlist rejects non-/api/ai paths,\n // turning every UI log fetch into a 400. agentLogsGet is the\n // narrow, read-only logs surface that satisfies the allowlist\n // without widening the AI proxy.\n query: `query AgentLogs($agentId: ID!, $profile: String, $query: JSON) {\n agentLogsGet(agentId: $agentId, profile: $profile, query: $query) {\n ok\n status\n data\n error\n }\n }`,\n variables: {\n agentId,\n profile,\n query: {\n level,\n limit: 200,\n ...(initialSource ? { source: initialSource } : {}),\n },\n },\n }),\n }\n );\n\n const payload = await response.json();\n const proxy = payload.data?.agentLogsGet;\n if (!proxy?.ok) {\n throw new Error(proxy?.error || payload.errors?.[0]?.message || 'Failed to fetch logs');\n }\n\n const nextEntries = ((proxy.data?.entries ?? []) as LogEntry[]).slice().sort((a, b) => {\n return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();\n });\n setEntries((current) => {\n const merged = new Map<string, LogEntry>();\n for (const entry of current) {\n merged.set(getEntryKey(entry), entry);\n }\n for (const entry of nextEntries) {\n merged.set(getEntryKey(entry), entry);\n }\n return Array.from(merged.values())\n .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())\n .slice(-maxEntries);\n });\n setConnected(true);\n setErrorMessage(null);\n\n if (autoScrollRef.current) {\n requestAnimationFrame(scrollToBottom);\n }\n } catch (error) {\n setConnected(false);\n setErrorMessage(\n error instanceof Error ? error.message : tr('logs.fetchFailed', 'Failed to fetch logs')\n );\n } finally {\n setIsRefreshing(false);\n }\n }, [\n agentId,\n profile,\n apiGatewayUrl,\n initialSource,\n level,\n maxEntries,\n scrollToBottom,\n tr,\n workspaceId,\n ]);\n\n useEffect(() => {\n setEntries([]);\n setConnected(false);\n setErrorMessage(null);\n }, [agentId, initialSource, level]);\n\n useEffect(() => {\n if (!agentId || paused) {\n return undefined;\n }\n\n let cancelled = false;\n const load = async () => {\n if (!cancelled) {\n await fetchLogs();\n }\n };\n\n void load();\n const intervalId = window.setInterval(() => {\n void load();\n }, 3000);\n\n return () => {\n cancelled = true;\n window.clearInterval(intervalId);\n };\n }, [agentId, fetchLogs, paused]);\n\n const filteredEntries = searchQuery\n ? entries.filter(\n (entry) =>\n entry.message.toLowerCase().includes(searchQuery.toLowerCase()) ||\n entry.source.toLowerCase().includes(searchQuery.toLowerCase())\n )\n : entries;\n\n const handleClear = () => setEntries([]);\n\n const handleExport = () => {\n const blob = new Blob([filteredEntries.map((entry) => JSON.stringify(entry)).join('\\n')], {\n type: 'application/x-ndjson',\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `agent-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.jsonl`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n const formatTime = (timestamp: string) => {\n try {\n const date = new Date(timestamp);\n return (\n date.toLocaleTimeString('en-US', {\n hour12: false,\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }) +\n '.' +\n String(date.getMilliseconds()).padStart(3, '0')\n );\n } catch {\n return timestamp;\n }\n };\n\n return (\n <div\n className={`flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${className}`}\n >\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2\">\n <div className=\"mr-2 flex items-center gap-1.5\">\n <div\n className={`size-2 rounded-full ${connected ? 'animate-pulse bg-status-success-bg' : 'bg-status-error-bg'}`}\n />\n <span className=\"text-xs text-text-tertiary\">\n {connected ? tr('logs.live', 'Live') : tr('logs.disconnected', 'Disconnected')}\n </span>\n </div>\n\n <select\n value={level}\n onChange={(event) => setLevel(event.target.value as LogEntry['level'])}\n className=\"rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary\"\n aria-label={tr('logs.levelFilter', 'Log level filter')}\n >\n <option value=\"debug\">DEBUG+</option>\n <option value=\"info\">INFO+</option>\n <option value=\"warn\">WARN+</option>\n <option value=\"error\">ERROR</option>\n </select>\n\n <div className=\"relative min-w-[120px] max-w-xs flex-1\">\n <Search\n size={12}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary\"\n />\n <input\n aria-label=\"control\"\n type=\"text\"\n placeholder={tr('logs.filterLogs', 'Filter logs...')}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full rounded border border-border-secondary bg-bg-tertiary py-1 pl-6 pr-2 text-xs text-text-primary placeholder:text-text-tertiary\"\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1\">\n <span className=\"text-xs text-text-tertiary\">\n {filteredEntries.length} {tr('logs.entries', 'entries')}\n </span>\n <button\n type=\"button\"\n onClick={() => void fetchLogs()}\n disabled={isRefreshing}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />\n </button>\n <button\n type=\"button\"\n onClick={() => setPaused((current) => !current)}\n className={`rounded p-1 transition-colors hover:bg-bg-tertiary ${paused ? 'text-status-warning-text' : 'text-text-tertiary'}`}\n title={paused ? tr('logs.resume', 'Resume') : tr('logs.pause', 'Pause')}\n >\n {paused ? <Play size={14} /> : <Pause size={14} />}\n </button>\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.clear', 'Clear')}\n >\n <Trash2 size={14} />\n </button>\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.export', 'Export')}\n >\n <Download size={14} />\n </button>\n </div>\n </div>\n\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text\"\n >\n {errorMessage}\n </div>\n ) : null}\n\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className={`overflow-y-auto font-mono text-xs leading-relaxed ${compact ? 'max-h-[300px]' : 'min-h-[400px] flex-1'}`}\n style={{ maxHeight: compact ? '300px' : undefined }}\n >\n {filteredEntries.length === 0 ? (\n <div className=\"flex h-full min-h-[200px] items-center justify-center text-text-tertiary\">\n {connected\n ? tr('logs.waitingForEntries', 'Waiting for log entries...')\n : tr('logs.connectToAgent', 'Connect to an agent to view logs')}\n </div>\n ) : (\n filteredEntries.map((entry, index) => (\n <div\n key={`${entry.timestamp}-${entry.source}-${index}`}\n className={`flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${LEVEL_BG[entry.level] ?? ''}`}\n >\n <span className=\"w-[85px] shrink-0 text-text-tertiary\">\n {formatTime(entry.timestamp)}\n </span>\n <span\n className={`w-[42px] shrink-0 uppercase font-semibold ${LEVEL_COLORS[entry.level] ?? 'text-text-primary'}`}\n >\n {entry.level}\n </span>\n <span\n className=\"w-[100px] shrink-0 truncate text-text-secondary\"\n title={entry.source}\n >\n [{entry.source}]\n </span>\n <span className=\"flex-1 break-all text-text-primary\">\n {entry.message}\n {entry.metadata && Object.keys(entry.metadata).length > 0 ? (\n <span className=\"ml-2 text-text-tertiary\">{JSON.stringify(entry.metadata)}</span>\n ) : null}\n </span>\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}