@burdenoff/microfe-vibecontrols 2026.529.12 → 2026.529.14

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,111 +1,86 @@
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";
1
+ import { useVibecontrolsAgentLogStreamSubscription as e } from "../../generated/wspace-operations.js";
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 { jsx as f, jsxs as p } from "react/jsx-runtime";
8
6
  //#region src/components/logs/LogViewer.tsx
9
- function _(e) {
7
+ function m(e) {
10
8
  return `${e.timestamp}:${e.level}:${e.source}:${e.message}`;
11
9
  }
12
- var v = {
10
+ var h = {
13
11
  debug: "text-text-tertiary",
14
12
  info: "text-status-info-text",
15
13
  warn: "text-status-warning-text",
16
14
  error: "text-status-error-text"
17
- }, y = {
15
+ }, g = {
18
16
  debug: "bg-bg-tertiary/30",
19
17
  info: "bg-status-info-bg/10",
20
18
  warn: "bg-status-warning-bg/10",
21
19
  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);
20
+ }, _ = ({ agentId: _, profile: v, level: y = "info", source: b, maxEntries: x = 1e3, className: S = "", compact: C = !1 }) => {
21
+ let { t: w } = a(), [T, E] = i([]), [D, O] = i(!1), [k, A] = i(y), [j, M] = i(""), [N, P] = i(null), F = r(null), I = r(!0), { data: L, error: R, loading: z } = e({
22
+ variables: {
23
+ agentId: _,
24
+ profile: v,
25
+ level: k,
26
+ source: b
27
+ },
28
+ skip: !_ || D,
29
+ shouldResubscribe: !0
30
+ }), B = (e, t) => {
31
+ let n = w(e);
25
32
  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);
35
- try {
36
- let e = p(), n = m(), r = await (await fetch(O ? `${O}/workspaces/graphql` : "/workspaces/graphql", {
37
- method: "POST",
38
- headers: {
39
- "Content-Type": "application/json",
40
- ...e ? { Authorization: `Bearer ${e}` } : {},
41
- ...n ? { "X-Workspace-Authorization": `Bearer ${n}` } : {},
42
- ...k ? { "x-workspace-id": k } : {}
43
- },
44
- 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 }",
46
- variables: {
47
- agentId: b,
48
- path: t(x, "/logs"),
49
- query: {
50
- level: I,
51
- limit: 200,
52
- ...C ? { source: C } : {}
53
- }
54
- }
55
- })
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) => {
60
- 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);
65
- } catch (e) {
66
- N(!1), V(e instanceof Error ? e.message : K("logs.fetchFailed", "Failed to fetch logs"));
67
- } finally {
68
- U(!1);
33
+ }, V = t(() => {
34
+ if (!F.current) return;
35
+ let e = F.current;
36
+ I.current = e.scrollHeight - e.scrollTop - e.clientHeight < 50;
37
+ }, []);
38
+ t(() => {
39
+ F.current && (F.current.scrollTop = F.current.scrollHeight);
40
+ }, []), n(() => {
41
+ let e = L?.vibecontrolsAgentLogStream;
42
+ if (!e || e.length === 0) return;
43
+ let t = e.map((e) => {
44
+ let t;
45
+ if (e.metadataJson) try {
46
+ t = JSON.parse(e.metadataJson);
47
+ } catch {
48
+ t = void 0;
69
49
  }
70
- }
71
- }, [
72
- b,
73
- x,
74
- O,
75
- C,
76
- I,
77
- w,
78
- J,
79
- K,
80
- k
81
- ]);
82
- r(() => {
83
- j([]), N(!1), V(null);
84
- }, [
85
- b,
86
- C,
87
- I
88
- ]), r(() => {
89
- if (!b || P) return;
90
- let e = !1, t = async () => {
91
- e || await Y();
92
- };
93
- t();
94
- let n = window.setInterval(() => {
95
- t();
96
- }, 3e3);
97
- return () => {
98
- e = !0, window.clearInterval(n);
99
- };
50
+ return {
51
+ timestamp: e.timestamp,
52
+ level: e.level ?? "info",
53
+ source: e.source ?? "",
54
+ message: e.message,
55
+ metadata: t
56
+ };
57
+ });
58
+ E((e) => {
59
+ let n = /* @__PURE__ */ new Map();
60
+ for (let t of e) n.set(m(t), t);
61
+ for (let e of t) n.set(m(e), e);
62
+ return Array.from(n.values()).sort((e, t) => new Date(e.timestamp).getTime() - new Date(t.timestamp).getTime()).slice(-x);
63
+ }), P(null), I.current && requestAnimationFrame(() => {
64
+ F.current && (F.current.scrollTop = F.current.scrollHeight);
65
+ });
66
+ }, [L, x]), n(() => {
67
+ R && P(R.message);
68
+ }, [R]);
69
+ let H = !R && !z, U = z;
70
+ n(() => {
71
+ E([]), P(null);
100
72
  }, [
101
- b,
102
- Y,
103
- P
73
+ _,
74
+ v,
75
+ k,
76
+ b
104
77
  ]);
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");
78
+ let W = t(() => {
79
+ E([]);
80
+ }, []), G = j ? T.filter((e) => e.message.toLowerCase().includes(j.toLowerCase()) || e.source.toLowerCase().includes(j.toLowerCase())) : T, K = () => E([]), q = () => {
81
+ let e = new Blob([G.map((e) => JSON.stringify(e)).join("\n")], { type: "application/x-ndjson" }), t = URL.createObjectURL(e), n = document.createElement("a");
107
82
  n.href = t, n.download = `agent-logs-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}.jsonl`, n.click(), URL.revokeObjectURL(t);
108
- }, $ = (e) => {
83
+ }, J = (e) => {
109
84
  try {
110
85
  let t = new Date(e);
111
86
  return t.toLocaleTimeString("en-US", {
@@ -118,129 +93,129 @@ var v = {
118
93
  return e;
119
94
  }
120
95
  };
121
- return /* @__PURE__ */ g("div", {
122
- className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${T}`,
96
+ return /* @__PURE__ */ p("div", {
97
+ className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${S}`,
123
98
  children: [
124
- /* @__PURE__ */ g("div", {
99
+ /* @__PURE__ */ p("div", {
125
100
  className: "flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2",
126
101
  children: [
127
- /* @__PURE__ */ g("div", {
102
+ /* @__PURE__ */ p("div", {
128
103
  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", {
104
+ children: [/* @__PURE__ */ f("div", { className: `size-2 rounded-full ${H ? "animate-pulse bg-status-success-bg" : "bg-status-error-bg"}` }), /* @__PURE__ */ f("span", {
130
105
  className: "text-xs text-text-tertiary",
131
- children: M ? K("logs.live", "Live") : K("logs.disconnected", "Disconnected")
106
+ children: H ? B("logs.live", "Live") : B("logs.disconnected", "Disconnected")
132
107
  })]
133
108
  }),
134
- /* @__PURE__ */ g("select", {
135
- value: I,
136
- onChange: (e) => L(e.target.value),
109
+ /* @__PURE__ */ p("select", {
110
+ value: k,
111
+ onChange: (e) => A(e.target.value),
137
112
  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"),
113
+ "aria-label": B("logs.levelFilter", "Log level filter"),
139
114
  children: [
140
- /* @__PURE__ */ h("option", {
115
+ /* @__PURE__ */ f("option", {
141
116
  value: "debug",
142
117
  children: "DEBUG+"
143
118
  }),
144
- /* @__PURE__ */ h("option", {
119
+ /* @__PURE__ */ f("option", {
145
120
  value: "info",
146
121
  children: "INFO+"
147
122
  }),
148
- /* @__PURE__ */ h("option", {
123
+ /* @__PURE__ */ f("option", {
149
124
  value: "warn",
150
125
  children: "WARN+"
151
126
  }),
152
- /* @__PURE__ */ h("option", {
127
+ /* @__PURE__ */ f("option", {
153
128
  value: "error",
154
129
  children: "ERROR"
155
130
  })
156
131
  ]
157
132
  }),
158
- /* @__PURE__ */ g("div", {
133
+ /* @__PURE__ */ p("div", {
159
134
  className: "relative min-w-[120px] max-w-xs flex-1",
160
- children: [/* @__PURE__ */ h(d, {
135
+ children: [/* @__PURE__ */ f(u, {
161
136
  size: 12,
162
137
  className: "absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary"
163
- }), /* @__PURE__ */ h("input", {
138
+ }), /* @__PURE__ */ f("input", {
164
139
  "aria-label": "control",
165
140
  type: "text",
166
- placeholder: K("logs.filterLogs", "Filter logs..."),
167
- value: R,
168
- onChange: (e) => z(e.target.value),
141
+ placeholder: B("logs.filterLogs", "Filter logs..."),
142
+ value: j,
143
+ onChange: (e) => M(e.target.value),
169
144
  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
145
  })]
171
146
  }),
172
- /* @__PURE__ */ g("div", {
147
+ /* @__PURE__ */ p("div", {
173
148
  className: "ml-auto flex items-center gap-1",
174
149
  children: [
175
- /* @__PURE__ */ g("span", {
150
+ /* @__PURE__ */ p("span", {
176
151
  className: "text-xs text-text-tertiary",
177
152
  children: [
178
- X.length,
153
+ G.length,
179
154
  " ",
180
- K("logs.entries", "entries")
155
+ B("logs.entries", "entries")
181
156
  ]
182
157
  }),
183
- /* @__PURE__ */ h("button", {
158
+ /* @__PURE__ */ f("button", {
184
159
  type: "button",
185
- onClick: () => void Y(),
186
- disabled: H,
160
+ onClick: () => void W(),
161
+ disabled: U,
187
162
  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, {
163
+ title: B("common.refresh", "Refresh"),
164
+ children: /* @__PURE__ */ f(l, {
190
165
  size: 14,
191
- className: H ? "animate-spin" : ""
166
+ className: U ? "animate-spin" : ""
192
167
  })
193
168
  }),
194
- /* @__PURE__ */ h("button", {
169
+ /* @__PURE__ */ f("button", {
195
170
  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 })
171
+ onClick: () => O((e) => !e),
172
+ className: `rounded p-1 transition-colors hover:bg-bg-tertiary ${D ? "text-status-warning-text" : "text-text-tertiary"}`,
173
+ title: D ? B("logs.resume", "Resume") : B("logs.pause", "Pause"),
174
+ children: f(D ? c : s, { size: 14 })
200
175
  }),
201
- /* @__PURE__ */ h("button", {
176
+ /* @__PURE__ */ f("button", {
202
177
  type: "button",
203
- onClick: Z,
178
+ onClick: K,
204
179
  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 })
180
+ title: B("logs.clear", "Clear"),
181
+ children: /* @__PURE__ */ f(d, { size: 14 })
207
182
  }),
208
- /* @__PURE__ */ h("button", {
183
+ /* @__PURE__ */ f("button", {
209
184
  type: "button",
210
- onClick: Q,
185
+ onClick: q,
211
186
  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 })
187
+ title: B("logs.export", "Export"),
188
+ children: /* @__PURE__ */ f(o, { size: 14 })
214
189
  })
215
190
  ]
216
191
  })
217
192
  ]
218
193
  }),
219
- B ? /* @__PURE__ */ h("div", {
194
+ N ? /* @__PURE__ */ f("div", {
220
195
  role: "alert",
221
196
  className: "border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text",
222
- children: B
197
+ children: N
223
198
  }) : 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", {
199
+ /* @__PURE__ */ f("div", {
200
+ ref: F,
201
+ onScroll: V,
202
+ className: `overflow-y-auto font-mono text-xs leading-relaxed ${C ? "max-h-[300px]" : "min-h-[400px] flex-1"}`,
203
+ style: { maxHeight: C ? "300px" : void 0 },
204
+ children: G.length === 0 ? /* @__PURE__ */ f("div", {
230
205
  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] ?? ""}`,
206
+ children: H ? B("logs.waitingForEntries", "Waiting for log entries...") : B("logs.connectToAgent", "Connect to an agent to view logs")
207
+ }) : G.map((e, t) => /* @__PURE__ */ p("div", {
208
+ className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${g[e.level] ?? ""}`,
234
209
  children: [
235
- /* @__PURE__ */ h("span", {
210
+ /* @__PURE__ */ f("span", {
236
211
  className: "w-[85px] shrink-0 text-text-tertiary",
237
- children: $(e.timestamp)
212
+ children: J(e.timestamp)
238
213
  }),
239
- /* @__PURE__ */ h("span", {
240
- className: `w-[42px] shrink-0 uppercase font-semibold ${v[e.level] ?? "text-text-primary"}`,
214
+ /* @__PURE__ */ f("span", {
215
+ className: `w-[42px] shrink-0 uppercase font-semibold ${h[e.level] ?? "text-text-primary"}`,
241
216
  children: e.level
242
217
  }),
243
- /* @__PURE__ */ g("span", {
218
+ /* @__PURE__ */ p("span", {
244
219
  className: "w-[100px] shrink-0 truncate text-text-secondary",
245
220
  title: e.source,
246
221
  children: [
@@ -249,9 +224,9 @@ var v = {
249
224
  "]"
250
225
  ]
251
226
  }),
252
- /* @__PURE__ */ g("span", {
227
+ /* @__PURE__ */ p("span", {
253
228
  className: "flex-1 break-all text-text-primary",
254
- children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */ h("span", {
229
+ children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */ f("span", {
255
230
  className: "ml-2 text-text-tertiary",
256
231
  children: JSON.stringify(e.metadata)
257
232
  }) : null]
@@ -263,6 +238,6 @@ var v = {
263
238
  });
264
239
  };
265
240
  //#endregion
266
- export { b as LogViewer };
241
+ export { _ as LogViewer };
267
242
 
268
243
  //# 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":"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"}
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 { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibecontrolsAgentLogStreamSubscription } from '@/generated/wspace-operations';\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 [entries, setEntries] = useState<LogEntry[]>([]);\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 containerRef = useRef<HTMLDivElement>(null);\n const autoScrollRef = useRef(true);\n\n // GraphQL subscription — the svc polls the agent server-side every\n // ~2s and pushes only new entries here. Replaces the previous client\n // setInterval + fetch, which was firing infinitely because\n // `tr` recreated on every render mutated fetchLogs' identity, which\n // re-ran the useEffect, which re-armed the interval, which called\n // fetchLogs immediately. The subscription has no client polling at\n // all — Apollo opens one WS, the svc owns the cadence.\n const {\n data: streamData,\n error: subscriptionError,\n loading: streamLoading,\n } = useVibecontrolsAgentLogStreamSubscription({\n variables: {\n agentId,\n profile,\n level,\n source: initialSource,\n },\n skip: !agentId || paused,\n shouldResubscribe: true,\n });\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 // Drain incoming subscription batches into the entries list. Each\n // payload is the NEW entries since the last server-side tick; merge\n // by key so re-deliveries (e.g. on reconnect backfill) don't dupe.\n useEffect(() => {\n const batch = streamData?.vibecontrolsAgentLogStream;\n if (!batch || batch.length === 0) return;\n const incoming: LogEntry[] = batch.map((row: { timestamp: string; level: string; source?: string | null; message: string; metadataJson?: string | null }) => {\n let metadata: Record<string, unknown> | undefined;\n if (row.metadataJson) {\n try {\n metadata = JSON.parse(row.metadataJson) as Record<string, unknown>;\n } catch {\n metadata = undefined;\n }\n }\n return {\n timestamp: row.timestamp,\n level: (row.level as LogEntry['level']) ?? 'info',\n source: row.source ?? '',\n message: row.message,\n metadata,\n };\n });\n setEntries((current) => {\n const merged = new Map<string, LogEntry>();\n for (const entry of current) merged.set(getEntryKey(entry), entry);\n for (const entry of incoming) merged.set(getEntryKey(entry), entry);\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 setErrorMessage(null);\n if (autoScrollRef.current) {\n requestAnimationFrame(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n });\n }\n }, [streamData, maxEntries]);\n\n useEffect(() => {\n if (subscriptionError) {\n setErrorMessage(subscriptionError.message);\n }\n }, [subscriptionError]);\n\n const connected = !subscriptionError && !streamLoading;\n const isRefreshing = streamLoading;\n\n // Reset the entries list when the agent / filter set actually changes.\n useEffect(() => {\n setEntries([]);\n setErrorMessage(null);\n }, [agentId, profile, level, initialSource]);\n\n // Manual refresh: no-op since the subscription pushes continuously.\n // Kept as a callback so the existing \"Refresh\" button doesn't have to\n // be conditionally hidden. Wraps in setEntries([]) so the operator's\n // intent of \"give me a fresh window\" is honoured — the next push\n // backfills.\n const fetchLogs = useCallback(() => {\n setEntries([]);\n }, []);\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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"}