@telorun/debug-ui 0.2.1 → 0.4.0
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/README.md +21 -6
- package/app-dist/assets/index-BMTuPr57.js +126 -0
- package/app-dist/assets/index-CWG3ZSzH.css +1 -0
- package/app-dist/index.html +2 -2
- package/app-single/index.html +48 -26
- package/package.json +4 -2
- package/src/components/DebugPanel.tsx +12 -3
- package/src/components/EventGraph.tsx +462 -0
- package/src/components/index.ts +1 -0
- package/src/components/styles.css +270 -0
- package/src/filter.ts +6 -2
- package/src/graph.test.ts +222 -0
- package/src/graph.ts +360 -0
- package/src/index.ts +16 -0
- package/app-dist/assets/index-CK8ArLzP.css +0 -1
- package/app-dist/assets/index-CwTwcQ4d.js +0 -104
package/src/graph.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { type DebugEvent, type DebugFrame, eventSuffix, isLogFrame } from "./wire.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure, framework-agnostic derivation of a resource graph from a debug event
|
|
5
|
+
* stream — the model behind the Graph view. No React, no layout: it folds the
|
|
6
|
+
* frame buffer into nodes (one per resource) and edges (dependency wiring), and
|
|
7
|
+
* tracks each node's lifecycle status and most-recent invocation. The component
|
|
8
|
+
* layer adds positions (dagre) and rendering.
|
|
9
|
+
*
|
|
10
|
+
* Two event families drive it:
|
|
11
|
+
* - Lifecycle events `<Kind>.<Name>.{Created,Initialized,Teardown}` — a node
|
|
12
|
+
* appears (its payload carries the resolved `{kind,name}` and `dependencies`),
|
|
13
|
+
* then flips to `initialized` / `torndown`.
|
|
14
|
+
* - Dispatch (trace) events — every capability call emits a structured payload
|
|
15
|
+
* `{ spanId, parentSpanId, capability, phase, outcome, ref:{kind,name}, … }`.
|
|
16
|
+
* The consumer reads the *payload*, never the dotted name: `ref` locates the
|
|
17
|
+
* node, `outcome` records the result. Terminal events carry an `outcome`;
|
|
18
|
+
* `phase:"start"` events carry none and are skipped here.
|
|
19
|
+
*
|
|
20
|
+
* When the producer has tracing on, dispatch events also carry `spanId` /
|
|
21
|
+
* `parentSpanId`; {@link deriveInvocations} folds those into the call tree behind
|
|
22
|
+
* the invocation list + scoped trace graph.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type NodeStatus = "created" | "initialized" | "torndown";
|
|
26
|
+
|
|
27
|
+
export type InvokeOutcome = "ok" | "failed" | "rejected" | "cancelled";
|
|
28
|
+
|
|
29
|
+
/** The most recent invocation observed for a node. */
|
|
30
|
+
export interface InvokeRecord {
|
|
31
|
+
outcome: InvokeOutcome;
|
|
32
|
+
/** The terminal event's suffix (`Invoked`, `InvokeFailed`, …) — for display only. */
|
|
33
|
+
suffix: string;
|
|
34
|
+
timestamp: string;
|
|
35
|
+
/** The dispatched inputs (present on every invocation event the kernel emits). */
|
|
36
|
+
inputs?: unknown;
|
|
37
|
+
/** The returned outputs — only on an `ok` outcome. */
|
|
38
|
+
outputs?: unknown;
|
|
39
|
+
/** The full raw event payload (error / cancellation detail for non-`ok`). */
|
|
40
|
+
payload: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GraphNode {
|
|
44
|
+
/** Stable id — the resource name (dot-free, unique within its module scope). */
|
|
45
|
+
id: string;
|
|
46
|
+
kind: string;
|
|
47
|
+
name: string;
|
|
48
|
+
module?: string;
|
|
49
|
+
status: NodeStatus;
|
|
50
|
+
/** Total invocations seen across the stream. */
|
|
51
|
+
invokeCount: number;
|
|
52
|
+
lastInvoke?: InvokeRecord;
|
|
53
|
+
/** Buffer index of the last frame that touched this node — drives the pulse:
|
|
54
|
+
* the renderer flashes a node whose activity index advanced since last paint. */
|
|
55
|
+
lastActivitySeq: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface GraphEdge {
|
|
59
|
+
id: string;
|
|
60
|
+
/** Source resource name (the dependant). */
|
|
61
|
+
source: string;
|
|
62
|
+
/** Target resource name (the dependency). */
|
|
63
|
+
target: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GraphState {
|
|
67
|
+
nodes: GraphNode[];
|
|
68
|
+
edges: GraphEdge[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface WireResourceRef {
|
|
72
|
+
kind: string;
|
|
73
|
+
name: string;
|
|
74
|
+
module?: string;
|
|
75
|
+
alias?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asResource(payload: unknown): WireResourceRef | undefined {
|
|
79
|
+
const res = (payload as { resource?: unknown } | undefined)?.resource;
|
|
80
|
+
if (res && typeof res === "object") {
|
|
81
|
+
const { kind, name } = res as Record<string, unknown>;
|
|
82
|
+
if (typeof kind === "string" && typeof name === "string") {
|
|
83
|
+
return { kind, name, ...res } as WireResourceRef;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The resolved `{kind,name}` a dispatch event's payload carries under `ref`. */
|
|
90
|
+
function readRef(payload: Record<string, unknown> | undefined): { kind: string; name: string } | undefined {
|
|
91
|
+
const ref = payload?.ref;
|
|
92
|
+
if (ref && typeof ref === "object") {
|
|
93
|
+
const { kind, name } = ref as Record<string, unknown>;
|
|
94
|
+
if (typeof kind === "string" && typeof name === "string") return { kind, name };
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Narrow a payload `outcome` to a known {@link InvokeOutcome}. Absent on
|
|
100
|
+
* `phase:"start"` events, so they are naturally skipped by callers. */
|
|
101
|
+
function asOutcome(value: unknown): InvokeOutcome | undefined {
|
|
102
|
+
return value === "ok" || value === "failed" || value === "rejected" || value === "cancelled"
|
|
103
|
+
? value
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function num(value: unknown): number | undefined {
|
|
108
|
+
return typeof value === "number" ? value : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function asCapability(value: unknown): SpanCapability | undefined {
|
|
112
|
+
return value === "invoke" || value === "run" || value === "provide" || value === "request"
|
|
113
|
+
? value
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fold a frame buffer into the resource graph. Logs are ignored; events are
|
|
119
|
+
* applied in arrival order so the returned status / `lastInvoke` reflect the
|
|
120
|
+
* latest state. Edges are deduped by `source→target` and retained even when the
|
|
121
|
+
* target node hasn't been seen (an imported dependency may live in another scope);
|
|
122
|
+
* the renderer drops dangling edges.
|
|
123
|
+
*/
|
|
124
|
+
export function deriveGraph(frames: readonly DebugFrame[]): GraphState {
|
|
125
|
+
const nodes = new Map<string, GraphNode>();
|
|
126
|
+
const edges = new Map<string, GraphEdge>();
|
|
127
|
+
|
|
128
|
+
frames.forEach((frame, seq) => {
|
|
129
|
+
if (isLogFrame(frame)) return;
|
|
130
|
+
const event = frame as DebugEvent;
|
|
131
|
+
const suffix = eventSuffix(event.event);
|
|
132
|
+
|
|
133
|
+
if (suffix === "Created") {
|
|
134
|
+
const res = asResource(event.payload);
|
|
135
|
+
if (!res) return;
|
|
136
|
+
const existing = nodes.get(res.name);
|
|
137
|
+
if (existing) {
|
|
138
|
+
existing.lastActivitySeq = seq;
|
|
139
|
+
} else {
|
|
140
|
+
nodes.set(res.name, {
|
|
141
|
+
id: res.name,
|
|
142
|
+
kind: res.kind,
|
|
143
|
+
name: res.name,
|
|
144
|
+
module: res.module,
|
|
145
|
+
status: "created",
|
|
146
|
+
invokeCount: 0,
|
|
147
|
+
lastActivitySeq: seq,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const deps = (event.payload as { dependencies?: WireResourceRef[] }).dependencies ?? [];
|
|
151
|
+
for (const dep of deps) {
|
|
152
|
+
if (!dep?.name || dep.name === res.name) continue;
|
|
153
|
+
const id = `${res.name}->${dep.name}`;
|
|
154
|
+
if (!edges.has(id)) edges.set(id, { id, source: res.name, target: dep.name });
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (suffix === "Initialized" || suffix === "Teardown") {
|
|
160
|
+
const res = asResource(event.payload);
|
|
161
|
+
const node = res ? nodes.get(res.name) : undefined;
|
|
162
|
+
if (node) {
|
|
163
|
+
node.status = suffix === "Teardown" ? "torndown" : "initialized";
|
|
164
|
+
node.lastActivitySeq = seq;
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Dispatch (trace) events: the payload's `ref.name` resolves the target node
|
|
170
|
+
// directly (nodes are name-keyed). `phase:"start"` events carry no `outcome`
|
|
171
|
+
// and are skipped — only terminal events record a result.
|
|
172
|
+
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
173
|
+
const ref = readRef(p);
|
|
174
|
+
const outcome = asOutcome(p?.outcome);
|
|
175
|
+
const node = ref ? nodes.get(ref.name) : undefined;
|
|
176
|
+
if (!node || !outcome) return;
|
|
177
|
+
node.invokeCount += 1;
|
|
178
|
+
node.lastActivitySeq = seq;
|
|
179
|
+
node.lastInvoke = {
|
|
180
|
+
outcome,
|
|
181
|
+
suffix: eventSuffix(event.event),
|
|
182
|
+
timestamp: event.timestamp,
|
|
183
|
+
inputs: p && "inputs" in p ? p.inputs : undefined,
|
|
184
|
+
outputs: p && "outputs" in p ? p.outputs : undefined,
|
|
185
|
+
payload: event.payload,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return { nodes: [...nodes.values()], edges: [...edges.values()] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Invocation traces ──────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/** Which capability the span represents. `"request"` is an inbound-boundary span. */
|
|
195
|
+
export type SpanCapability = "invoke" | "run" | "provide" | "request";
|
|
196
|
+
|
|
197
|
+
/** One invocation — a single dispatched call, identified by its `spanId`. */
|
|
198
|
+
export interface Invocation {
|
|
199
|
+
id: number;
|
|
200
|
+
parentId?: number;
|
|
201
|
+
/** Trace this span belongs to — present while tracing; groups the call tree. */
|
|
202
|
+
traceId?: string;
|
|
203
|
+
kind: string;
|
|
204
|
+
name: string;
|
|
205
|
+
capability?: SpanCapability;
|
|
206
|
+
outcome: InvokeOutcome;
|
|
207
|
+
/** The terminal event's suffix (`Invoked`, `InvokeFailed`, …) — for display only. */
|
|
208
|
+
suffix: string;
|
|
209
|
+
/** Human label (e.g. a route `"POST /feedback"`), for `request` spans. */
|
|
210
|
+
label?: string;
|
|
211
|
+
/** Structured span attributes (e.g. `{ method, path }`). */
|
|
212
|
+
attributes?: Record<string, unknown>;
|
|
213
|
+
/** On a trace root: the redacted CEL root scope (variables / secrets / resources
|
|
214
|
+
* / ports) the trace could reference. */
|
|
215
|
+
context?: Record<string, unknown>;
|
|
216
|
+
timestamp: string;
|
|
217
|
+
inputs?: unknown;
|
|
218
|
+
outputs?: unknown;
|
|
219
|
+
/** Full raw event payload (error / cancellation detail for non-`ok`). */
|
|
220
|
+
payload: unknown;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface TraceState {
|
|
224
|
+
/** Every invocation, keyed by id. */
|
|
225
|
+
byId: Map<number, Invocation>;
|
|
226
|
+
/** Child invocation ids per parent id. */
|
|
227
|
+
childrenOf: Map<number, number[]>;
|
|
228
|
+
/** Root invocations (no parent), in arrival order. */
|
|
229
|
+
roots: Invocation[];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fold the dispatch events that carry a `spanId` into a call tree. Each
|
|
234
|
+
* invocation appears once — keyed by `spanId`, the first terminal event wins, so
|
|
235
|
+
* the `InvokeRejected.Undeclared` echo (same id, emitted after `InvokeRejected`)
|
|
236
|
+
* is deduped. `phase:"start"` events carry no `outcome` and are skipped. Empty
|
|
237
|
+
* when the producer isn't tracing (no `spanId`).
|
|
238
|
+
*/
|
|
239
|
+
export function deriveInvocations(frames: readonly DebugFrame[]): TraceState {
|
|
240
|
+
const byId = new Map<number, Invocation>();
|
|
241
|
+
const childrenOf = new Map<number, number[]>();
|
|
242
|
+
const roots: Invocation[] = [];
|
|
243
|
+
|
|
244
|
+
for (const frame of frames) {
|
|
245
|
+
if (isLogFrame(frame)) continue;
|
|
246
|
+
const event = frame as DebugEvent;
|
|
247
|
+
const p = (event.payload ?? undefined) as Record<string, unknown> | undefined;
|
|
248
|
+
const id = num(p?.spanId);
|
|
249
|
+
if (id === undefined || byId.has(id)) continue;
|
|
250
|
+
const outcome = asOutcome(p?.outcome);
|
|
251
|
+
if (!outcome) continue;
|
|
252
|
+
const ref = readRef(p);
|
|
253
|
+
if (!ref) continue;
|
|
254
|
+
|
|
255
|
+
const parentId = num(p?.parentSpanId);
|
|
256
|
+
const invocation: Invocation = {
|
|
257
|
+
id,
|
|
258
|
+
parentId,
|
|
259
|
+
traceId: typeof p?.traceId === "string" ? p.traceId : undefined,
|
|
260
|
+
kind: ref.kind,
|
|
261
|
+
name: ref.name,
|
|
262
|
+
capability: asCapability(p?.capability),
|
|
263
|
+
outcome,
|
|
264
|
+
suffix: eventSuffix(event.event),
|
|
265
|
+
label: typeof p?.label === "string" ? p.label : undefined,
|
|
266
|
+
attributes:
|
|
267
|
+
p?.attributes && typeof p.attributes === "object"
|
|
268
|
+
? (p.attributes as Record<string, unknown>)
|
|
269
|
+
: undefined,
|
|
270
|
+
context:
|
|
271
|
+
p?.context && typeof p.context === "object"
|
|
272
|
+
? (p.context as Record<string, unknown>)
|
|
273
|
+
: undefined,
|
|
274
|
+
timestamp: event.timestamp,
|
|
275
|
+
inputs: p && "inputs" in p ? p.inputs : undefined,
|
|
276
|
+
outputs: p && "outputs" in p ? p.outputs : undefined,
|
|
277
|
+
payload: event.payload,
|
|
278
|
+
};
|
|
279
|
+
byId.set(id, invocation);
|
|
280
|
+
if (parentId === undefined) {
|
|
281
|
+
roots.push(invocation);
|
|
282
|
+
} else {
|
|
283
|
+
const list = childrenOf.get(parentId);
|
|
284
|
+
if (list) list.push(id);
|
|
285
|
+
else childrenOf.set(parentId, [id]);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { byId, childrenOf, roots };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** One resource in a scoped trace graph — collapses repeated calls to the same
|
|
293
|
+
* resource into a single node carrying all its invocations within that trace. */
|
|
294
|
+
export interface TraceNode {
|
|
295
|
+
/** Resource key `kind.name`. */
|
|
296
|
+
id: string;
|
|
297
|
+
kind: string;
|
|
298
|
+
name: string;
|
|
299
|
+
/** Human label from the span (e.g. a route `"POST /feedback"`), if any. */
|
|
300
|
+
label?: string;
|
|
301
|
+
isRoot: boolean;
|
|
302
|
+
invocations: Invocation[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface TraceEdge {
|
|
306
|
+
id: string;
|
|
307
|
+
source: string;
|
|
308
|
+
target: string;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface TraceSubgraph {
|
|
312
|
+
nodes: TraceNode[];
|
|
313
|
+
edges: TraceEdge[];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The call graph of a single trace: the resources that participated in `rootId`'s
|
|
318
|
+
* subtree, wired by the actual parent→child call edges. A resource invoked more
|
|
319
|
+
* than once in the trace is one node holding all its calls; edges are deduped by
|
|
320
|
+
* `source→target`.
|
|
321
|
+
*/
|
|
322
|
+
export function traceSubgraph(trace: TraceState, rootId: number): TraceSubgraph {
|
|
323
|
+
const root = trace.byId.get(rootId);
|
|
324
|
+
if (!root) return { nodes: [], edges: [] };
|
|
325
|
+
|
|
326
|
+
const resourceKey = (inv: Invocation) => `${inv.kind}.${inv.name}`;
|
|
327
|
+
const nodes = new Map<string, TraceNode>();
|
|
328
|
+
const edges = new Map<string, TraceEdge>();
|
|
329
|
+
|
|
330
|
+
const visit = (id: number): void => {
|
|
331
|
+
const inv = trace.byId.get(id);
|
|
332
|
+
if (!inv) return;
|
|
333
|
+
const key = resourceKey(inv);
|
|
334
|
+
const node = nodes.get(key);
|
|
335
|
+
if (node) node.invocations.push(inv);
|
|
336
|
+
else
|
|
337
|
+
nodes.set(key, {
|
|
338
|
+
id: key,
|
|
339
|
+
kind: inv.kind,
|
|
340
|
+
name: inv.name,
|
|
341
|
+
label: inv.label,
|
|
342
|
+
isRoot: id === rootId,
|
|
343
|
+
invocations: [inv],
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
for (const childId of trace.childrenOf.get(id) ?? []) {
|
|
347
|
+
const child = trace.byId.get(childId);
|
|
348
|
+
if (!child) continue;
|
|
349
|
+
const target = resourceKey(child);
|
|
350
|
+
if (key !== target) {
|
|
351
|
+
const edgeId = `${key}->${target}`;
|
|
352
|
+
if (!edges.has(edgeId)) edges.set(edgeId, { id: edgeId, source: key, target });
|
|
353
|
+
}
|
|
354
|
+
visit(childId);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
visit(rootId);
|
|
358
|
+
|
|
359
|
+
return { nodes: [...nodes.values()], edges: [...edges.values()] };
|
|
360
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,3 +19,19 @@ export { type EventFilter, matchesFilter, distinctSuffixes } from "./filter.js";
|
|
|
19
19
|
export type { DebugTheme } from "./theme.js";
|
|
20
20
|
export { type DebugStreamHandlers, connectDebugStream } from "./sse-client.js";
|
|
21
21
|
export { type FoundBlob, collectBlobs, blobDimensions, formatBytes } from "./media.js";
|
|
22
|
+
export {
|
|
23
|
+
type GraphState,
|
|
24
|
+
type GraphNode,
|
|
25
|
+
type GraphEdge,
|
|
26
|
+
type NodeStatus,
|
|
27
|
+
type InvokeOutcome,
|
|
28
|
+
type InvokeRecord,
|
|
29
|
+
type Invocation,
|
|
30
|
+
type TraceState,
|
|
31
|
+
type TraceNode,
|
|
32
|
+
type TraceEdge,
|
|
33
|
+
type TraceSubgraph,
|
|
34
|
+
deriveGraph,
|
|
35
|
+
deriveInvocations,
|
|
36
|
+
traceSubgraph,
|
|
37
|
+
} from "./graph.js";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.tdbg-root{--tdbg-bg: #0f1115;--tdbg-panel: #161a21;--tdbg-line: #232a34;--tdbg-text: #d7dce3;--tdbg-muted: #7a8493;--tdbg-accent: #4f9cf0;--tdbg-bad: #ff8079;--tdbg-bad-bg: rgba(248, 81, 73, .2);--tdbg-good: #5fd673;--tdbg-good-bg: rgba(63, 185, 80, .18);display:flex;flex-direction:column;height:100%;min-height:0;font:13px/1.5 ui-monospace,SF Mono,Menlo,Consolas,monospace;color:var(--tdbg-text);background:var(--tdbg-bg)}.tdbg-root[data-theme=light]{--tdbg-bg: #ffffff;--tdbg-panel: #f6f7f9;--tdbg-line: #e2e5e9;--tdbg-text: #1f2328;--tdbg-muted: #656d76;--tdbg-accent: #2f6feb;--tdbg-bad: #cf222e;--tdbg-bad-bg: rgba(207, 34, 46, .1);--tdbg-good: #1a7f37;--tdbg-good-bg: rgba(26, 127, 55, .1)}.tdbg-tabs{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--tdbg-panel);border-bottom:1px solid var(--tdbg-line)}.tdbg-tab{padding:4px 10px;color:var(--tdbg-muted);background:none;border:1px solid transparent;border-radius:6px;cursor:pointer;font:inherit}.tdbg-tab:hover{color:var(--tdbg-text)}.tdbg-tab-on{color:var(--tdbg-text);background:var(--tdbg-bg);border-color:var(--tdbg-line)}.tdbg-tabcount{margin-left:4px;color:var(--tdbg-muted);font-size:11px}.tdbg-tabs-spacer{flex:1}.tdbg-endpoints{display:flex;align-items:center;gap:6px}.tdbg-endpoint{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;color:var(--tdbg-muted);background:var(--tdbg-bg);border:1px solid var(--tdbg-line);border-radius:999px;font-size:11px;white-space:nowrap}.tdbg-endpoint-link{color:var(--tdbg-accent);text-decoration:none}.tdbg-endpoint-link:hover{border-color:var(--tdbg-accent)}.tdbg-bar{position:sticky;top:0;z-index:1;padding:8px 10px;background:var(--tdbg-panel);border-bottom:1px solid var(--tdbg-line)}.tdbg-bar-row{display:flex;align-items:center;gap:8px}.tdbg-search{flex:1}.tdbg-search,.tdbg-kind{padding:5px 8px;color:var(--tdbg-text);background:var(--tdbg-bg);border:1px solid var(--tdbg-line);border-radius:5px;font:inherit}.tdbg-kind{width:140px}.tdbg-btn{padding:5px 10px;color:var(--tdbg-text);background:var(--tdbg-bg);border:1px solid var(--tdbg-line);border-radius:5px;cursor:pointer;font:inherit}.tdbg-btn:hover{border-color:var(--tdbg-accent)}.tdbg-icon-btn{display:inline-flex;align-items:center;justify-content:center;padding:5px 7px}.tdbg-counts{color:var(--tdbg-muted);white-space:nowrap}.tdbg-status{width:9px;height:9px;border-radius:50%;background:var(--tdbg-muted);flex:0 0 auto}.tdbg-status-open{background:#3fb950}.tdbg-status-connecting{background:#d29922}.tdbg-status-closed{background:#f85149}.tdbg-facets{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.tdbg-facet{padding:2px 8px;color:var(--tdbg-muted);background:var(--tdbg-bg);border:1px solid var(--tdbg-line);border-radius:999px;cursor:pointer;font:inherit}.tdbg-facet-on{color:var(--tdbg-text);border-color:var(--tdbg-accent);background:#4f9cf02e}.tdbg-table{flex:1;min-height:0;overflow:auto}.tdbg-empty{padding:24px;color:var(--tdbg-muted);text-align:center}.tdbg-row{border-bottom:1px solid var(--tdbg-line)}.tdbg-rowhead{display:flex;align-items:baseline;gap:8px;width:100%;padding:4px 10px;background:none;border:none;color:inherit;cursor:pointer;text-align:left;font:inherit}.tdbg-rowhead:hover{background:var(--tdbg-panel)}.tdbg-caret{width:12px;color:var(--tdbg-muted)}.tdbg-time{color:var(--tdbg-muted);white-space:nowrap}.tdbg-badge{padding:1px 6px;border-radius:4px;background:var(--tdbg-line);color:var(--tdbg-text);white-space:nowrap}.tdbg-suffix-failed,.tdbg-suffix-rejected,.tdbg-suffix-cancelled{background:var(--tdbg-bad-bg);color:var(--tdbg-bad)}.tdbg-suffix-invoked,.tdbg-suffix-listening,.tdbg-suffix-started,.tdbg-suffix-initialized{background:var(--tdbg-good-bg);color:var(--tdbg-good)}.tdbg-name{white-space:nowrap}.tdbg-preview{color:var(--tdbg-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tdbg-payload-wrap{background:var(--tdbg-panel)}.tdbg-payload{margin:0;padding:8px 10px 10px 30px;background:var(--tdbg-panel);overflow:auto;white-space:pre-wrap;word-break:break-word}.tdbg-media{display:flex;flex-wrap:wrap;gap:12px;padding:10px 10px 4px 30px}.tdbg-blob{margin:0;display:flex;flex-direction:column;gap:4px;max-width:320px}.tdbg-blob-img{max-width:320px;max-height:240px;border:1px solid var(--tdbg-line);border-radius:6px;background:repeating-conic-gradient(#1b202840 0% 25%,transparent 0% 50%) 50% / 16px 16px;object-fit:contain}.tdbg-blob-file{display:inline-block;padding:8px 12px;color:var(--tdbg-accent);border:1px solid var(--tdbg-line);border-radius:6px;text-decoration:none}.tdbg-blob-file:hover{border-color:var(--tdbg-accent)}.tdbg-blob-cap{color:var(--tdbg-muted);font-size:11px;word-break:break-all}.tdbg-muted{color:var(--tdbg-muted)}.tdbg-ref{color:var(--tdbg-accent)}.tdbg-slot{display:flex;flex:1;min-height:0;overflow:hidden}.tdbg-logs{flex:1;min-height:0;overflow:auto;padding:4px 0}.tdbg-logline{display:flex;gap:8px;padding:1px 10px;white-space:pre-wrap;word-break:break-word}.tdbg-logline:hover{background:var(--tdbg-panel)}.tdbg-logtext{flex:1}.tdbg-log-stderr .tdbg-logtext{color:var(--tdbg-bad)}
|