@burdenoff/microfe-vibecontrols 2026.529.13 → 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.
- package/dist/components/logs/LogViewer.js +125 -149
- package/dist/components/logs/LogViewer.js.map +1 -1
- package/dist/generated/wspace-operations.js +71 -48
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +10 -10
- package/package.json +1 -1
|
@@ -1,110 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useVibecontrolsAgentLogStreamSubscription as e } from "../../generated/wspace-operations.js";
|
|
2
2
|
import { useCallback as t, useEffect as n, useRef as r, useState as i } from "react";
|
|
3
3
|
import { useI18n as a } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
4
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 {
|
|
6
|
-
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
5
|
+
import { jsx as f, jsxs as p } from "react/jsx-runtime";
|
|
7
6
|
//#region src/components/logs/LogViewer.tsx
|
|
8
|
-
function
|
|
7
|
+
function m(e) {
|
|
9
8
|
return `${e.timestamp}:${e.level}:${e.source}:${e.message}`;
|
|
10
9
|
}
|
|
11
|
-
var
|
|
10
|
+
var h = {
|
|
12
11
|
debug: "text-text-tertiary",
|
|
13
12
|
info: "text-status-info-text",
|
|
14
13
|
warn: "text-status-warning-text",
|
|
15
14
|
error: "text-status-error-text"
|
|
16
|
-
},
|
|
15
|
+
}, g = {
|
|
17
16
|
debug: "bg-bg-tertiary/30",
|
|
18
17
|
info: "bg-status-info-bg/10",
|
|
19
18
|
warn: "bg-status-warning-bg/10",
|
|
20
19
|
error: "bg-status-error-bg/10"
|
|
21
|
-
},
|
|
22
|
-
let { t:
|
|
23
|
-
|
|
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);
|
|
24
32
|
return n === e ? t : n;
|
|
25
|
-
},
|
|
26
|
-
if (!
|
|
27
|
-
let e =
|
|
28
|
-
|
|
29
|
-
}, [])
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
...O ? { "x-workspace-id": O } : {}
|
|
42
|
-
},
|
|
43
|
-
body: JSON.stringify({
|
|
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 }",
|
|
45
|
-
variables: {
|
|
46
|
-
agentId: y,
|
|
47
|
-
profile: b,
|
|
48
|
-
query: {
|
|
49
|
-
level: F,
|
|
50
|
-
limit: 200,
|
|
51
|
-
...S ? { source: S } : {}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
})
|
|
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) => {
|
|
59
|
-
let t = /* @__PURE__ */ new Map();
|
|
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);
|
|
64
|
-
} catch (e) {
|
|
65
|
-
M(!1), B(e instanceof Error ? e.message : G("logs.fetchFailed", "Failed to fetch logs"));
|
|
66
|
-
} finally {
|
|
67
|
-
H(!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;
|
|
68
49
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
70
|
n(() => {
|
|
82
|
-
|
|
83
|
-
}, [
|
|
84
|
-
y,
|
|
85
|
-
S,
|
|
86
|
-
F
|
|
87
|
-
]), n(() => {
|
|
88
|
-
if (!y || N) return;
|
|
89
|
-
let e = !1, t = async () => {
|
|
90
|
-
e || await J();
|
|
91
|
-
};
|
|
92
|
-
t();
|
|
93
|
-
let n = window.setInterval(() => {
|
|
94
|
-
t();
|
|
95
|
-
}, 3e3);
|
|
96
|
-
return () => {
|
|
97
|
-
e = !0, window.clearInterval(n);
|
|
98
|
-
};
|
|
71
|
+
E([]), P(null);
|
|
99
72
|
}, [
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
73
|
+
_,
|
|
74
|
+
v,
|
|
75
|
+
k,
|
|
76
|
+
b
|
|
103
77
|
]);
|
|
104
|
-
let
|
|
105
|
-
|
|
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");
|
|
106
82
|
n.href = t, n.download = `agent-logs-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-")}.jsonl`, n.click(), URL.revokeObjectURL(t);
|
|
107
|
-
},
|
|
83
|
+
}, J = (e) => {
|
|
108
84
|
try {
|
|
109
85
|
let t = new Date(e);
|
|
110
86
|
return t.toLocaleTimeString("en-US", {
|
|
@@ -117,129 +93,129 @@ var _ = {
|
|
|
117
93
|
return e;
|
|
118
94
|
}
|
|
119
95
|
};
|
|
120
|
-
return /* @__PURE__ */
|
|
121
|
-
className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${
|
|
96
|
+
return /* @__PURE__ */ p("div", {
|
|
97
|
+
className: `flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${S}`,
|
|
122
98
|
children: [
|
|
123
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ p("div", {
|
|
124
100
|
className: "flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2",
|
|
125
101
|
children: [
|
|
126
|
-
/* @__PURE__ */
|
|
102
|
+
/* @__PURE__ */ p("div", {
|
|
127
103
|
className: "mr-2 flex items-center gap-1.5",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
104
|
+
children: [/* @__PURE__ */ f("div", { className: `size-2 rounded-full ${H ? "animate-pulse bg-status-success-bg" : "bg-status-error-bg"}` }), /* @__PURE__ */ f("span", {
|
|
129
105
|
className: "text-xs text-text-tertiary",
|
|
130
|
-
children:
|
|
106
|
+
children: H ? B("logs.live", "Live") : B("logs.disconnected", "Disconnected")
|
|
131
107
|
})]
|
|
132
108
|
}),
|
|
133
|
-
/* @__PURE__ */
|
|
134
|
-
value:
|
|
135
|
-
onChange: (e) =>
|
|
109
|
+
/* @__PURE__ */ p("select", {
|
|
110
|
+
value: k,
|
|
111
|
+
onChange: (e) => A(e.target.value),
|
|
136
112
|
className: "rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary",
|
|
137
|
-
"aria-label":
|
|
113
|
+
"aria-label": B("logs.levelFilter", "Log level filter"),
|
|
138
114
|
children: [
|
|
139
|
-
/* @__PURE__ */
|
|
115
|
+
/* @__PURE__ */ f("option", {
|
|
140
116
|
value: "debug",
|
|
141
117
|
children: "DEBUG+"
|
|
142
118
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
119
|
+
/* @__PURE__ */ f("option", {
|
|
144
120
|
value: "info",
|
|
145
121
|
children: "INFO+"
|
|
146
122
|
}),
|
|
147
|
-
/* @__PURE__ */
|
|
123
|
+
/* @__PURE__ */ f("option", {
|
|
148
124
|
value: "warn",
|
|
149
125
|
children: "WARN+"
|
|
150
126
|
}),
|
|
151
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ f("option", {
|
|
152
128
|
value: "error",
|
|
153
129
|
children: "ERROR"
|
|
154
130
|
})
|
|
155
131
|
]
|
|
156
132
|
}),
|
|
157
|
-
/* @__PURE__ */
|
|
133
|
+
/* @__PURE__ */ p("div", {
|
|
158
134
|
className: "relative min-w-[120px] max-w-xs flex-1",
|
|
159
|
-
children: [/* @__PURE__ */
|
|
135
|
+
children: [/* @__PURE__ */ f(u, {
|
|
160
136
|
size: 12,
|
|
161
137
|
className: "absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary"
|
|
162
|
-
}), /* @__PURE__ */
|
|
138
|
+
}), /* @__PURE__ */ f("input", {
|
|
163
139
|
"aria-label": "control",
|
|
164
140
|
type: "text",
|
|
165
|
-
placeholder:
|
|
166
|
-
value:
|
|
167
|
-
onChange: (e) =>
|
|
141
|
+
placeholder: B("logs.filterLogs", "Filter logs..."),
|
|
142
|
+
value: j,
|
|
143
|
+
onChange: (e) => M(e.target.value),
|
|
168
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"
|
|
169
145
|
})]
|
|
170
146
|
}),
|
|
171
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ p("div", {
|
|
172
148
|
className: "ml-auto flex items-center gap-1",
|
|
173
149
|
children: [
|
|
174
|
-
/* @__PURE__ */
|
|
150
|
+
/* @__PURE__ */ p("span", {
|
|
175
151
|
className: "text-xs text-text-tertiary",
|
|
176
152
|
children: [
|
|
177
|
-
|
|
153
|
+
G.length,
|
|
178
154
|
" ",
|
|
179
|
-
|
|
155
|
+
B("logs.entries", "entries")
|
|
180
156
|
]
|
|
181
157
|
}),
|
|
182
|
-
/* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ f("button", {
|
|
183
159
|
type: "button",
|
|
184
|
-
onClick: () => void
|
|
185
|
-
disabled:
|
|
160
|
+
onClick: () => void W(),
|
|
161
|
+
disabled: U,
|
|
186
162
|
className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50",
|
|
187
|
-
title:
|
|
188
|
-
children: /* @__PURE__ */
|
|
163
|
+
title: B("common.refresh", "Refresh"),
|
|
164
|
+
children: /* @__PURE__ */ f(l, {
|
|
189
165
|
size: 14,
|
|
190
|
-
className:
|
|
166
|
+
className: U ? "animate-spin" : ""
|
|
191
167
|
})
|
|
192
168
|
}),
|
|
193
|
-
/* @__PURE__ */
|
|
169
|
+
/* @__PURE__ */ f("button", {
|
|
194
170
|
type: "button",
|
|
195
|
-
onClick: () =>
|
|
196
|
-
className: `rounded p-1 transition-colors hover:bg-bg-tertiary ${
|
|
197
|
-
title:
|
|
198
|
-
children:
|
|
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 })
|
|
199
175
|
}),
|
|
200
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ f("button", {
|
|
201
177
|
type: "button",
|
|
202
|
-
onClick:
|
|
178
|
+
onClick: K,
|
|
203
179
|
className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary",
|
|
204
|
-
title:
|
|
205
|
-
children: /* @__PURE__ */
|
|
180
|
+
title: B("logs.clear", "Clear"),
|
|
181
|
+
children: /* @__PURE__ */ f(d, { size: 14 })
|
|
206
182
|
}),
|
|
207
|
-
/* @__PURE__ */
|
|
183
|
+
/* @__PURE__ */ f("button", {
|
|
208
184
|
type: "button",
|
|
209
|
-
onClick:
|
|
185
|
+
onClick: q,
|
|
210
186
|
className: "rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary",
|
|
211
|
-
title:
|
|
212
|
-
children: /* @__PURE__ */
|
|
187
|
+
title: B("logs.export", "Export"),
|
|
188
|
+
children: /* @__PURE__ */ f(o, { size: 14 })
|
|
213
189
|
})
|
|
214
190
|
]
|
|
215
191
|
})
|
|
216
192
|
]
|
|
217
193
|
}),
|
|
218
|
-
|
|
194
|
+
N ? /* @__PURE__ */ f("div", {
|
|
219
195
|
role: "alert",
|
|
220
196
|
className: "border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text",
|
|
221
|
-
children:
|
|
197
|
+
children: N
|
|
222
198
|
}) : null,
|
|
223
|
-
/* @__PURE__ */
|
|
224
|
-
ref:
|
|
225
|
-
onScroll:
|
|
226
|
-
className: `overflow-y-auto font-mono text-xs leading-relaxed ${
|
|
227
|
-
style: { maxHeight:
|
|
228
|
-
children:
|
|
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", {
|
|
229
205
|
className: "flex h-full min-h-[200px] items-center justify-center text-text-tertiary",
|
|
230
|
-
children:
|
|
231
|
-
}) :
|
|
232
|
-
className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${
|
|
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] ?? ""}`,
|
|
233
209
|
children: [
|
|
234
|
-
/* @__PURE__ */
|
|
210
|
+
/* @__PURE__ */ f("span", {
|
|
235
211
|
className: "w-[85px] shrink-0 text-text-tertiary",
|
|
236
|
-
children:
|
|
212
|
+
children: J(e.timestamp)
|
|
237
213
|
}),
|
|
238
|
-
/* @__PURE__ */
|
|
239
|
-
className: `w-[42px] shrink-0 uppercase font-semibold ${
|
|
214
|
+
/* @__PURE__ */ f("span", {
|
|
215
|
+
className: `w-[42px] shrink-0 uppercase font-semibold ${h[e.level] ?? "text-text-primary"}`,
|
|
240
216
|
children: e.level
|
|
241
217
|
}),
|
|
242
|
-
/* @__PURE__ */
|
|
218
|
+
/* @__PURE__ */ p("span", {
|
|
243
219
|
className: "w-[100px] shrink-0 truncate text-text-secondary",
|
|
244
220
|
title: e.source,
|
|
245
221
|
children: [
|
|
@@ -248,9 +224,9 @@ var _ = {
|
|
|
248
224
|
"]"
|
|
249
225
|
]
|
|
250
226
|
}),
|
|
251
|
-
/* @__PURE__ */
|
|
227
|
+
/* @__PURE__ */ p("span", {
|
|
252
228
|
className: "flex-1 break-all text-text-primary",
|
|
253
|
-
children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */
|
|
229
|
+
children: [e.message, e.metadata && Object.keys(e.metadata).length > 0 ? /* @__PURE__ */ f("span", {
|
|
254
230
|
className: "ml-2 text-text-tertiary",
|
|
255
231
|
children: JSON.stringify(e.metadata)
|
|
256
232
|
}) : null]
|
|
@@ -262,6 +238,6 @@ var _ = {
|
|
|
262
238
|
});
|
|
263
239
|
};
|
|
264
240
|
//#endregion
|
|
265
|
-
export {
|
|
241
|
+
export { _ as LogViewer };
|
|
266
242
|
|
|
267
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 // 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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"}
|
|
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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"}
|