@burdenoff/microfe-vibecontrols 2026.529.14 → 2026.529.16

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,32 +1,32 @@
1
- import { useGetAgentConnectionEventsQuery as e } from "../../generated/wspace-operations.js";
2
- import { useCallback as t, useMemo as n, useState as r } from "react";
3
- import { jsx as i, jsxs as a } from "react/jsx-runtime";
1
+ import { useGetAgentConnectionEventsQuery as e, useGetSessionConnectionEventsQuery as t } from "../../generated/wspace-operations.js";
2
+ import { useCallback as n, useMemo as r, useState as i } from "react";
3
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
4
4
  //#region src/components/agents/ConnectionLogPanel.tsx
5
- var o = {
5
+ var s = {
6
6
  info: "text-status-info-text",
7
7
  warn: "text-status-warning-text",
8
8
  error: "text-status-danger-text",
9
9
  debug: "text-fg-muted"
10
- }, s = {
10
+ }, c = {
11
11
  info: "bg-status-info-bg",
12
12
  warn: "bg-status-warning-bg",
13
13
  error: "bg-status-danger-bg",
14
14
  debug: "bg-bg-muted"
15
15
  };
16
- function c(e) {
16
+ function l(e) {
17
17
  try {
18
18
  return new Date(e).toISOString().replace("T", " ").replace("Z", "");
19
19
  } catch {
20
20
  return e;
21
21
  }
22
22
  }
23
- function l(e) {
23
+ function u(e) {
24
24
  return e.map((e) => {
25
25
  let t = e.detailsJson ? `\n details: ${e.detailsJson}` : "";
26
26
  return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${t}`;
27
27
  }).join("\n");
28
28
  }
29
- function u(e, t, n) {
29
+ function d(e, t, n) {
30
30
  return JSON.stringify({
31
31
  exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
32
32
  agentId: t,
@@ -43,19 +43,28 @@ function u(e, t, n) {
43
43
  }))
44
44
  }, null, 2);
45
45
  }
46
- function d(e, t, n) {
46
+ function f(e, t, n) {
47
47
  let r = new Blob([n], { type: t }), i = URL.createObjectURL(r), a = document.createElement("a");
48
48
  a.href = i, a.download = e, document.body.appendChild(a), a.click(), document.body.removeChild(a), URL.revokeObjectURL(i);
49
49
  }
50
- function f({ agentId: f, agentName: p }) {
51
- let { data: m, loading: h, error: g, refetch: _ } = e({
50
+ function p({ agentId: p, sessionId: m, agentName: h, sessionName: g }) {
51
+ let _ = !!p, v = (p ?? m) || "", y = _ ? h : g, b = e({
52
+ variables: {
53
+ agentId: p ?? "",
54
+ limit: 200
55
+ },
56
+ pollInterval: 3e3,
57
+ fetchPolicy: "cache-and-network",
58
+ skip: !p
59
+ }), x = t({
52
60
  variables: {
53
- agentId: f,
61
+ sessionId: m ?? "",
54
62
  limit: 200
55
63
  },
56
64
  pollInterval: 3e3,
57
- fetchPolicy: "cache-and-network"
58
- }), [v, y] = r("none"), b = n(() => (m?.vibecontrolsAgentConnectionEvents ?? []).map((e) => ({
65
+ fetchPolicy: "cache-and-network",
66
+ skip: !m
67
+ }), { data: S, loading: C, error: w, refetch: T } = _ ? b : x, [E, D] = i("none"), O = r(() => (_ ? b.data?.vibecontrolsAgentConnectionEvents ?? [] : x.data?.vibecontrolsSessionConnectionEvents ?? []).map((e) => ({
59
68
  id: e.id,
60
69
  timestamp: e.timestamp,
61
70
  sequence: e.sequence,
@@ -63,89 +72,95 @@ function f({ agentId: f, agentName: p }) {
63
72
  level: e.level,
64
73
  message: e.message,
65
74
  detailsJson: e.detailsJson ?? null
66
- })), [m]), x = t(async (e) => {
67
- let t = e === "text" ? l(b) : u(b, f, p);
75
+ })), [
76
+ _,
77
+ b.data,
78
+ x.data
79
+ ]), k = n(async (e) => {
80
+ let t = e === "text" ? u(O) : d(O, v, y);
68
81
  try {
69
- await navigator.clipboard.writeText(t), y(e), setTimeout(() => y("none"), 1500);
82
+ await navigator.clipboard.writeText(t), D(e), setTimeout(() => D("none"), 1500);
70
83
  } catch {
71
- d(`agent-${f}-connection-log.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
84
+ f(`${_ ? "agent" : "session"}-${v}-connection-log.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
72
85
  }
73
86
  }, [
74
- b,
75
- f,
76
- p
77
- ]), S = t((e) => {
78
- let t = e === "text" ? l(b) : u(b, f, p), n = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
79
- d(`agent-${p ?? f}-connection-log-${n}.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
87
+ O,
88
+ v,
89
+ y,
90
+ _
91
+ ]), A = n((e) => {
92
+ let t = e === "text" ? u(O) : d(O, v, y), n = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
93
+ f(`${_ ? "agent" : "session"}-${y ?? v}-connection-log-${n}.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
80
94
  }, [
81
- b,
82
- f,
83
- p
95
+ O,
96
+ v,
97
+ y,
98
+ _
84
99
  ]);
85
- return /* @__PURE__ */ a("div", {
100
+ return /* @__PURE__ */ o("div", {
86
101
  className: "flex flex-col h-full",
87
102
  children: [
88
- /* @__PURE__ */ a("div", {
103
+ /* @__PURE__ */ o("div", {
89
104
  className: "flex items-center justify-between p-4 border-b border-border-default",
90
- children: [/* @__PURE__ */ a("div", { children: [/* @__PURE__ */ i("h3", {
105
+ children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h3", {
91
106
  className: "text-fg-default font-semibold text-base",
92
107
  children: "Connection Log"
93
- }), /* @__PURE__ */ i("p", {
108
+ }), /* @__PURE__ */ a("p", {
94
109
  className: "text-fg-muted text-xs mt-0.5",
95
- children: h && b.length === 0 ? "Loading…" : `${b.length} event${b.length === 1 ? "" : "s"} · auto-refresh every 3s`
96
- })] }), /* @__PURE__ */ a("div", {
110
+ children: C && O.length === 0 ? "Loading…" : `${O.length} event${O.length === 1 ? "" : "s"} · auto-refresh every 3s`
111
+ })] }), /* @__PURE__ */ o("div", {
97
112
  className: "flex gap-2",
98
113
  children: [
99
- /* @__PURE__ */ i("button", {
114
+ /* @__PURE__ */ a("button", {
100
115
  type: "button",
101
- onClick: () => _(),
116
+ onClick: () => T(),
102
117
  className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
103
118
  children: "Refresh"
104
119
  }),
105
- /* @__PURE__ */ i("button", {
120
+ /* @__PURE__ */ a("button", {
106
121
  type: "button",
107
- onClick: () => x("text"),
122
+ onClick: () => k("text"),
108
123
  className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
109
- children: v === "text" ? "Copied!" : "Copy text"
124
+ children: E === "text" ? "Copied!" : "Copy text"
110
125
  }),
111
- /* @__PURE__ */ i("button", {
126
+ /* @__PURE__ */ a("button", {
112
127
  type: "button",
113
- onClick: () => x("json"),
128
+ onClick: () => k("json"),
114
129
  className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
115
- children: v === "json" ? "Copied!" : "Copy JSON"
130
+ children: E === "json" ? "Copied!" : "Copy JSON"
116
131
  }),
117
- /* @__PURE__ */ i("button", {
132
+ /* @__PURE__ */ a("button", {
118
133
  type: "button",
119
- onClick: () => S("text"),
134
+ onClick: () => A("text"),
120
135
  className: "px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary",
121
136
  children: "Export .txt"
122
137
  }),
123
- /* @__PURE__ */ i("button", {
138
+ /* @__PURE__ */ a("button", {
124
139
  type: "button",
125
- onClick: () => S("json"),
140
+ onClick: () => A("json"),
126
141
  className: "px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary",
127
142
  children: "Export .json"
128
143
  })
129
144
  ]
130
145
  })]
131
146
  }),
132
- g && /* @__PURE__ */ a("div", {
147
+ w && /* @__PURE__ */ o("div", {
133
148
  className: "m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm",
134
- children: ["Failed to load events: ", g.message]
149
+ children: ["Failed to load events: ", w.message]
135
150
  }),
136
- /* @__PURE__ */ a("div", {
151
+ /* @__PURE__ */ o("div", {
137
152
  className: "flex-1 overflow-auto p-4",
138
- children: [b.length === 0 && !h && /* @__PURE__ */ a("div", {
153
+ children: [O.length === 0 && !C && /* @__PURE__ */ o("div", {
139
154
  className: "text-fg-muted text-sm text-center py-12",
140
155
  children: [
141
156
  "No connection events recorded for this agent yet.",
142
- /* @__PURE__ */ i("br", {}),
157
+ /* @__PURE__ */ a("br", {}),
143
158
  "Events appear here every time the platform probes, registers, or syncs the agent."
144
159
  ]
145
- }), /* @__PURE__ */ i("ol", {
160
+ }), /* @__PURE__ */ a("ol", {
146
161
  className: "space-y-2",
147
- children: b.map((e) => {
148
- let t = o[e.level] ?? o.info, n = s[e.level] ?? s.info, r = (() => {
162
+ children: O.map((e) => {
163
+ let t = s[e.level] ?? s.info, n = c[e.level] ?? c.info, r = (() => {
149
164
  if (!e.detailsJson) return null;
150
165
  try {
151
166
  return JSON.stringify(JSON.parse(e.detailsJson), null, 2);
@@ -153,39 +168,39 @@ function f({ agentId: f, agentName: p }) {
153
168
  return e.detailsJson;
154
169
  }
155
170
  })();
156
- return /* @__PURE__ */ i("li", {
171
+ return /* @__PURE__ */ a("li", {
157
172
  className: "rounded border border-border-default bg-bg-subtle",
158
- children: /* @__PURE__ */ a("div", {
173
+ children: /* @__PURE__ */ o("div", {
159
174
  className: "flex items-start gap-3 p-3",
160
- children: [/* @__PURE__ */ i("div", {
175
+ children: [/* @__PURE__ */ a("div", {
161
176
  className: `px-2 py-0.5 rounded text-[10px] uppercase font-mono ${n} ${t} shrink-0`,
162
177
  children: e.level
163
- }), /* @__PURE__ */ a("div", {
178
+ }), /* @__PURE__ */ o("div", {
164
179
  className: "flex-1 min-w-0",
165
180
  children: [
166
- /* @__PURE__ */ a("div", {
181
+ /* @__PURE__ */ o("div", {
167
182
  className: "flex items-center gap-2 text-xs text-fg-muted font-mono",
168
183
  children: [
169
- /* @__PURE__ */ a("span", { children: ["#", e.sequence] }),
170
- /* @__PURE__ */ i("span", { children: "·" }),
171
- /* @__PURE__ */ i("span", { children: c(e.timestamp) }),
172
- /* @__PURE__ */ i("span", { children: "·" }),
173
- /* @__PURE__ */ i("span", {
184
+ /* @__PURE__ */ o("span", { children: ["#", e.sequence] }),
185
+ /* @__PURE__ */ a("span", { children: "·" }),
186
+ /* @__PURE__ */ a("span", { children: l(e.timestamp) }),
187
+ /* @__PURE__ */ a("span", { children: "·" }),
188
+ /* @__PURE__ */ a("span", {
174
189
  className: "text-fg-default font-medium",
175
190
  children: e.step
176
191
  })
177
192
  ]
178
193
  }),
179
- /* @__PURE__ */ i("div", {
194
+ /* @__PURE__ */ a("div", {
180
195
  className: "text-sm text-fg-default mt-1 break-words",
181
196
  children: e.message
182
197
  }),
183
- r && /* @__PURE__ */ a("details", {
198
+ r && /* @__PURE__ */ o("details", {
184
199
  className: "mt-2",
185
- children: [/* @__PURE__ */ i("summary", {
200
+ children: [/* @__PURE__ */ a("summary", {
186
201
  className: "text-xs text-fg-muted cursor-pointer hover:text-fg-default",
187
202
  children: "Details"
188
- }), /* @__PURE__ */ i("pre", {
203
+ }), /* @__PURE__ */ a("pre", {
189
204
  className: "mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words",
190
205
  children: r
191
206
  })]
@@ -201,6 +216,6 @@ function f({ agentId: f, agentName: p }) {
201
216
  });
202
217
  }
203
218
  //#endregion
204
- export { f as ConnectionLogPanel };
219
+ export { p as ConnectionLogPanel };
205
220
 
206
221
  //# sourceMappingURL=ConnectionLogPanel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectionLogPanel.js","names":[],"sources":["../../../src/components/agents/ConnectionLogPanel.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { useGetAgentConnectionEventsQuery } from '@/generated/wspace-operations';\n\ninterface Props {\n agentId: string;\n agentName?: string;\n}\n\ninterface EventRow {\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n}\n\nconst LEVEL_COLOR: Record<string, string> = {\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-danger-text',\n debug: 'text-fg-muted',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n info: 'bg-status-info-bg',\n warn: 'bg-status-warning-bg',\n error: 'bg-status-danger-bg',\n debug: 'bg-bg-muted',\n};\n\nfunction formatTimestamp(iso: string): string {\n try {\n return new Date(iso).toISOString().replace('T', ' ').replace('Z', '');\n } catch {\n return iso;\n }\n}\n\nfunction eventsAsText(rows: EventRow[]): string {\n return rows\n .map((e) => {\n const detail = e.detailsJson ? `\\n details: ${e.detailsJson}` : '';\n return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${detail}`;\n })\n .join('\\n');\n}\n\nfunction eventsAsJson(rows: EventRow[], agentId: string, agentName?: string): string {\n return JSON.stringify(\n {\n exportedAt: new Date().toISOString(),\n agentId,\n agentName,\n count: rows.length,\n events: rows.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n details: e.detailsJson ? JSON.parse(e.detailsJson) : null,\n })),\n },\n null,\n 2,\n );\n}\n\nfunction downloadBlob(filename: string, mime: string, body: string): void {\n const blob = new Blob([body], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nexport function ConnectionLogPanel({ agentId, agentName }: Props) {\n // Poll every 3s while the panel is mounted so the operator sees new\n // events appear as the svc records them — no manual refresh needed\n // while debugging \"stuck\" agents.\n const { data, loading, error, refetch } = useGetAgentConnectionEventsQuery({\n variables: { agentId, limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n });\n const [copied, setCopied] = useState<'none' | 'text' | 'json'>('none');\n\n const events: EventRow[] = useMemo(() => {\n const raw = data?.vibecontrolsAgentConnectionEvents ?? [];\n return raw.map((e: { id: string; timestamp: string; sequence: number; step: string; level: string; message: string; detailsJson?: string | null }) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n detailsJson: e.detailsJson ?? null,\n }));\n }, [data]);\n\n const handleCopy = useCallback(\n async (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, agentId, agentName);\n try {\n await navigator.clipboard.writeText(body);\n setCopied(mode);\n setTimeout(() => setCopied('none'), 1500);\n } catch {\n /* clipboard denied — fall back to download */\n downloadBlob(`agent-${agentId}-connection-log.${mode === 'text' ? 'txt' : 'json'}`, mode === 'text' ? 'text/plain' : 'application/json', body);\n }\n },\n [events, agentId, agentName],\n );\n\n const handleDownload = useCallback(\n (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, agentId, agentName);\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n downloadBlob(\n `agent-${agentName ?? agentId}-connection-log-${stamp}.${mode === 'text' ? 'txt' : 'json'}`,\n mode === 'text' ? 'text/plain' : 'application/json',\n body,\n );\n },\n [events, agentId, agentName],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between p-4 border-b border-border-default\">\n <div>\n <h3 className=\"text-fg-default font-semibold text-base\">Connection Log</h3>\n <p className=\"text-fg-muted text-xs mt-0.5\">\n {loading && events.length === 0\n ? 'Loading…'\n : `${events.length} event${events.length === 1 ? '' : 's'} · auto-refresh every 3s`}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'text' ? 'Copied!' : 'Copy text'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'json' ? 'Copied!' : 'Copy JSON'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .txt\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .json\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm\">\n Failed to load events: {error.message}\n </div>\n )}\n\n <div className=\"flex-1 overflow-auto p-4\">\n {events.length === 0 && !loading && (\n <div className=\"text-fg-muted text-sm text-center py-12\">\n No connection events recorded for this agent yet.\n <br />\n Events appear here every time the platform probes, registers, or syncs the agent.\n </div>\n )}\n <ol className=\"space-y-2\">\n {events.map((e) => {\n const levelColor = LEVEL_COLOR[e.level] ?? LEVEL_COLOR.info;\n const levelBg = LEVEL_BG[e.level] ?? LEVEL_BG.info;\n const details = (() => {\n if (!e.detailsJson) return null;\n try {\n return JSON.stringify(JSON.parse(e.detailsJson), null, 2);\n } catch {\n return e.detailsJson;\n }\n })();\n return (\n <li\n key={e.id}\n className=\"rounded border border-border-default bg-bg-subtle\"\n >\n <div className=\"flex items-start gap-3 p-3\">\n <div className={`px-2 py-0.5 rounded text-[10px] uppercase font-mono ${levelBg} ${levelColor} shrink-0`}>\n {e.level}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 text-xs text-fg-muted font-mono\">\n <span>#{e.sequence}</span>\n <span>·</span>\n <span>{formatTimestamp(e.timestamp)}</span>\n <span>·</span>\n <span className=\"text-fg-default font-medium\">{e.step}</span>\n </div>\n <div className=\"text-sm text-fg-default mt-1 break-words\">{e.message}</div>\n {details && (\n <details className=\"mt-2\">\n <summary className=\"text-xs text-fg-muted cursor-pointer hover:text-fg-default\">\n Details\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words\">\n {details}\n </pre>\n </details>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ol>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ConnectionLogPanel.js","names":[],"sources":["../../../src/components/agents/ConnectionLogPanel.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport {\n useGetAgentConnectionEventsQuery,\n useGetSessionConnectionEventsQuery,\n} from '@/generated/wspace-operations';\n\ninterface Props {\n /** Pass agentId XOR sessionId. */\n agentId?: string;\n sessionId?: string;\n /** Friendly label shown in the export filename. */\n agentName?: string;\n sessionName?: string;\n}\n\ninterface EventRow {\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n}\n\nconst LEVEL_COLOR: Record<string, string> = {\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-danger-text',\n debug: 'text-fg-muted',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n info: 'bg-status-info-bg',\n warn: 'bg-status-warning-bg',\n error: 'bg-status-danger-bg',\n debug: 'bg-bg-muted',\n};\n\nfunction formatTimestamp(iso: string): string {\n try {\n return new Date(iso).toISOString().replace('T', ' ').replace('Z', '');\n } catch {\n return iso;\n }\n}\n\nfunction eventsAsText(rows: EventRow[]): string {\n return rows\n .map((e) => {\n const detail = e.detailsJson ? `\\n details: ${e.detailsJson}` : '';\n return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${detail}`;\n })\n .join('\\n');\n}\n\nfunction eventsAsJson(rows: EventRow[], agentId: string, agentName?: string): string {\n return JSON.stringify(\n {\n exportedAt: new Date().toISOString(),\n agentId,\n agentName,\n count: rows.length,\n events: rows.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n details: e.detailsJson ? JSON.parse(e.detailsJson) : null,\n })),\n },\n null,\n 2,\n );\n}\n\nfunction downloadBlob(filename: string, mime: string, body: string): void {\n const blob = new Blob([body], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nexport function ConnectionLogPanel({ agentId, sessionId, agentName, sessionName }: Props) {\n // Poll every 3s while the panel is mounted so the operator sees new\n // events appear as the svc records them. The same panel powers both\n // the AgentDetails \"Connection Log\" tab (pass agentId) and the\n // SessionDetails \"Connection Log\" tab (pass sessionId). Apollo\n // skips the query whose id is missing.\n const isAgent = !!agentId;\n const subjectId = (agentId ?? sessionId) || '';\n const subjectLabel = isAgent ? agentName : sessionName;\n\n const agentQuery = useGetAgentConnectionEventsQuery({\n variables: { agentId: agentId ?? '', limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n skip: !agentId,\n });\n const sessionQuery = useGetSessionConnectionEventsQuery({\n variables: { sessionId: sessionId ?? '', limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n skip: !sessionId,\n });\n const { data, loading, error, refetch } = isAgent ? agentQuery : sessionQuery;\n const [copied, setCopied] = useState<'none' | 'text' | 'json'>('none');\n\n const events: EventRow[] = useMemo(() => {\n const rawList: Array<{\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n }> = isAgent\n ? (agentQuery.data?.vibecontrolsAgentConnectionEvents ?? [])\n : (sessionQuery.data?.vibecontrolsSessionConnectionEvents ?? []);\n return rawList.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n detailsJson: e.detailsJson ?? null,\n }));\n }, [isAgent, agentQuery.data, sessionQuery.data]);\n\n const handleCopy = useCallback(\n async (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, subjectId, subjectLabel);\n try {\n await navigator.clipboard.writeText(body);\n setCopied(mode);\n setTimeout(() => setCopied('none'), 1500);\n } catch {\n const filename = `${isAgent ? 'agent' : 'session'}-${subjectId}-connection-log.${\n mode === 'text' ? 'txt' : 'json'\n }`;\n downloadBlob(filename, mode === 'text' ? 'text/plain' : 'application/json', body);\n }\n },\n [events, subjectId, subjectLabel, isAgent],\n );\n\n const handleDownload = useCallback(\n (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, subjectId, subjectLabel);\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const prefix = isAgent ? 'agent' : 'session';\n downloadBlob(\n `${prefix}-${subjectLabel ?? subjectId}-connection-log-${stamp}.${mode === 'text' ? 'txt' : 'json'}`,\n mode === 'text' ? 'text/plain' : 'application/json',\n body,\n );\n },\n [events, subjectId, subjectLabel, isAgent],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between p-4 border-b border-border-default\">\n <div>\n <h3 className=\"text-fg-default font-semibold text-base\">Connection Log</h3>\n <p className=\"text-fg-muted text-xs mt-0.5\">\n {loading && events.length === 0\n ? 'Loading…'\n : `${events.length} event${events.length === 1 ? '' : 's'} · auto-refresh every 3s`}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'text' ? 'Copied!' : 'Copy text'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'json' ? 'Copied!' : 'Copy JSON'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .txt\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .json\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm\">\n Failed to load events: {error.message}\n </div>\n )}\n\n <div className=\"flex-1 overflow-auto p-4\">\n {events.length === 0 && !loading && (\n <div className=\"text-fg-muted text-sm text-center py-12\">\n No connection events recorded for this agent yet.\n <br />\n Events appear here every time the platform probes, registers, or syncs the agent.\n </div>\n )}\n <ol className=\"space-y-2\">\n {events.map((e) => {\n const levelColor = LEVEL_COLOR[e.level] ?? LEVEL_COLOR.info;\n const levelBg = LEVEL_BG[e.level] ?? LEVEL_BG.info;\n const details = (() => {\n if (!e.detailsJson) return null;\n try {\n return JSON.stringify(JSON.parse(e.detailsJson), null, 2);\n } catch {\n return e.detailsJson;\n }\n })();\n return (\n <li\n key={e.id}\n className=\"rounded border border-border-default bg-bg-subtle\"\n >\n <div className=\"flex items-start gap-3 p-3\">\n <div className={`px-2 py-0.5 rounded text-[10px] uppercase font-mono ${levelBg} ${levelColor} shrink-0`}>\n {e.level}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 text-xs text-fg-muted font-mono\">\n <span>#{e.sequence}</span>\n <span>·</span>\n <span>{formatTimestamp(e.timestamp)}</span>\n <span>·</span>\n <span className=\"text-fg-default font-medium\">{e.step}</span>\n </div>\n <div className=\"text-sm text-fg-default mt-1 break-words\">{e.message}</div>\n {details && (\n <details className=\"mt-2\">\n <summary className=\"text-xs text-fg-muted cursor-pointer hover:text-fg-default\">\n Details\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words\">\n {details}\n </pre>\n </details>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ol>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -203,7 +203,7 @@ var h = {
203
203
  style: { maxHeight: C ? "300px" : void 0 },
204
204
  children: G.length === 0 ? /* @__PURE__ */ f("div", {
205
205
  className: "flex h-full min-h-[200px] items-center justify-center text-text-tertiary",
206
- children: H ? B("logs.waitingForEntries", "Waiting for log entries...") : B("logs.connectToAgent", "Connect to an agent to view logs")
206
+ children: _ ? N ? `${B("logs.subscriptionError", "Log stream error")}: ${N}` : z ? B("logs.connecting", "Connecting to log stream…") : B("logs.waitingForEntries", "Waiting for log entries") : B("logs.connectToAgent", "Connect to an agent to view logs")
207
207
  }) : G.map((e, t) => /* @__PURE__ */ p("div", {
208
208
  className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${g[e.level] ?? ""}`,
209
209
  children: [
@@ -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 { 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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"}
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 {!agentId\n ? tr('logs.connectToAgent', 'Connect to an agent to view logs')\n : errorMessage\n ? `${tr('logs.subscriptionError', 'Log stream error')}: ${errorMessage}`\n : streamLoading\n ? tr('logs.connecting', 'Connecting to log stream…')\n : tr('logs.waitingForEntries', 'Waiting for log entries…')}\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":";;;;;;AAwBA,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,CAAC,GAAS,KAAc,EAAqB,EAAE,CAAC,EAChD,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,IAAe,EAAuB,KAAK,EAC3C,IAAgB,EAAO,GAAK,EAS5B,EACJ,MAAM,GACN,OAAO,GACP,SAAS,MACP,EAA0C;EAC5C,WAAW;GACT;GACA;GACA;GACA,QAAQ;GACT;EACD,MAAM,CAAC,KAAW;EAClB,mBAAmB;EACpB,CAAC,EAEI,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;AAiDN,CA/CuB,QAAkB;AACvC,EAAI,EAAa,YACf,EAAa,QAAQ,YAAY,EAAa,QAAQ;IAEvD,EAAE,CAAC,EAKN,QAAgB;EACd,IAAM,IAAQ,GAAY;AAC1B,MAAI,CAAC,KAAS,EAAM,WAAW,EAAG;EAClC,IAAM,IAAuB,EAAM,KAAK,MAAqH;GAC3J,IAAI;AACJ,OAAI,EAAI,aACN,KAAI;AACF,QAAW,KAAK,MAAM,EAAI,aAAa;WACjC;AACN,QAAW,KAAA;;AAGf,UAAO;IACL,WAAW,EAAI;IACf,OAAQ,EAAI,SAA+B;IAC3C,QAAQ,EAAI,UAAU;IACtB,SAAS,EAAI;IACb;IACD;IACD;AAUF,EATA,GAAY,MAAY;GACtB,IAAM,oBAAS,IAAI,KAAuB;AAC1C,QAAK,IAAM,KAAS,EAAS,GAAO,IAAI,EAAY,EAAM,EAAE,EAAM;AAClE,QAAK,IAAM,KAAS,EAAU,GAAO,IAAI,EAAY,EAAM,EAAE,EAAM;AACnE,UAAO,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;IACrB,EACF,EAAgB,KAAK,EACjB,EAAc,WAChB,4BAA4B;AAC1B,GAAI,EAAa,YACf,EAAa,QAAQ,YAAY,EAAa,QAAQ;IAExD;IAEH,CAAC,GAAY,EAAW,CAAC,EAE5B,QAAgB;AACd,EAAI,KACF,EAAgB,EAAkB,QAAQ;IAE3C,CAAC,EAAkB,CAAC;CAEvB,IAAM,IAAY,CAAC,KAAqB,CAAC,GACnC,IAAe;AAGrB,SAAgB;AAEd,EADA,EAAW,EAAE,CAAC,EACd,EAAgB,KAAK;IACpB;EAAC;EAAS;EAAS;EAAO;EAAc,CAAC;CAO5C,IAAM,IAAY,QAAkB;AAClC,IAAW,EAAE,CAAC;IACb,EAAE,CAAC,EAEA,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;eACX,IAEE,IACE,GAAG,EAAG,0BAA0B,mBAAmB,CAAC,IAAI,MACxD,IACE,EAAG,mBAAmB,4BAA4B,GAClD,EAAG,0BAA0B,2BAA2B,GAL5D,EAAG,uBAAuB,mCAAmC;KAM7D,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"}