@oh-my-pi/omp-stats 18.13.3 → 18.14.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/CHANGELOG.md +10 -0
- package/README.md +18 -18
- package/dist/client/index.js +78 -78
- package/dist/types/chunked-insert.d.ts +32 -0
- package/dist/types/client/traces/SpanDrawer.d.ts +9 -3
- package/dist/types/client/traces/TimelineCanvas.d.ts +3 -1
- package/dist/types/client/traces/TraceView.d.ts +13 -0
- package/dist/types/client/traces/index.d.ts +5 -0
- package/dist/types/client/traces/trace-colors.d.ts +3 -1
- package/dist/types/client/ui/SearchInput.d.ts +2 -1
- package/dist/types/db.d.ts +5 -5
- package/dist/types/port-conflict.d.ts +9 -3
- package/dist/types/server.d.ts +7 -3
- package/dist/types/shared-types.d.ts +3 -2
- package/dist/types/trace.d.ts +27 -1
- package/dist/types/types.d.ts +6 -1
- package/package.json +10 -5
- package/src/chunked-insert.ts +89 -0
- package/src/client/traces/SpanDrawer.tsx +56 -9
- package/src/client/traces/TimelineCanvas.tsx +13 -4
- package/src/client/traces/TraceView.tsx +165 -158
- package/src/client/traces/index.ts +5 -0
- package/src/client/traces/trace-colors.ts +14 -10
- package/src/client/ui/SearchInput.tsx +3 -1
- package/src/db.ts +197 -147
- package/src/parser.ts +13 -2
- package/src/port-conflict.ts +67 -30
- package/src/server.ts +39 -31
- package/src/shared-types.ts +3 -2
- package/src/trace.ts +119 -35
- package/src/types.ts +6 -1
- package/THIRD-PARTY-NOTICES.txt +0 -22901
|
@@ -12,7 +12,7 @@ import type { SessionTrace, TraceSpan, TraceSpanKind, TraceTrack } from "../type
|
|
|
12
12
|
import { Card, ChartSkeleton, PageHeader, QueryView, SearchInput, Segmented } from "../ui";
|
|
13
13
|
import { AggregatesPanel } from "./AggregatesPanel";
|
|
14
14
|
import { Minimap } from "./Minimap";
|
|
15
|
-
import { SpanDrawer } from "./SpanDrawer";
|
|
15
|
+
import { SpanDrawer, type TraceEntryLoader } from "./SpanDrawer";
|
|
16
16
|
import { SummaryStrip } from "./SummaryStrip";
|
|
17
17
|
import { TimelineCanvas, type TimelineViewport } from "./TimelineCanvas";
|
|
18
18
|
import { TranscriptList } from "./TranscriptList";
|
|
@@ -48,6 +48,56 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
48
48
|
// A previous session's trace must never render under this file's header.
|
|
49
49
|
const trace = query.stale ? null : query.data;
|
|
50
50
|
|
|
51
|
+
return (
|
|
52
|
+
<div className="stack traces-view">
|
|
53
|
+
<div>
|
|
54
|
+
<button type="button" className="btn" data-variant="ghost" data-size="sm" onClick={onBack}>
|
|
55
|
+
<ArrowLeft size={14} aria-hidden="true" />
|
|
56
|
+
Sessions
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<PageHeader
|
|
60
|
+
title={<span className="traces-title truncate">{trace?.title ?? file.split("/").pop()}</span>}
|
|
61
|
+
description={trace?.cwd ? <span className="mono">{trace.cwd}</span> : undefined}
|
|
62
|
+
actions={
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className="btn"
|
|
66
|
+
data-variant="ghost"
|
|
67
|
+
data-icon="true"
|
|
68
|
+
onClick={query.refetch}
|
|
69
|
+
aria-label="Refresh trace"
|
|
70
|
+
title="Refresh trace"
|
|
71
|
+
>
|
|
72
|
+
<RefreshCw size={14} className={query.refreshing ? "traces-spin" : undefined} />
|
|
73
|
+
</button>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
<QueryView query={{ ...query, data: trace }} skeleton={<ChartSkeleton height={420} />}>
|
|
77
|
+
{loaded => <SessionTraceView trace={loaded} />}
|
|
78
|
+
</QueryView>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Data and host integration supplied to the shared live/offline trace viewer. */
|
|
84
|
+
export interface SessionTraceViewProps {
|
|
85
|
+
trace: SessionTrace;
|
|
86
|
+
view?: "timeline" | "full";
|
|
87
|
+
loadEntry?: TraceEntryLoader;
|
|
88
|
+
/** Embedded viewers keep details inside their own layout, without a modal. */
|
|
89
|
+
embedded?: boolean;
|
|
90
|
+
onNavigate?: (trackId: string, entryId: string) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Data-driven viewer shared by the live route and self-contained HTML exports. */
|
|
94
|
+
export function SessionTraceView({
|
|
95
|
+
trace,
|
|
96
|
+
view = "full",
|
|
97
|
+
loadEntry,
|
|
98
|
+
embedded = false,
|
|
99
|
+
onNavigate,
|
|
100
|
+
}: SessionTraceViewProps) {
|
|
51
101
|
const [mode, setMode] = useState<AxisMode>("time");
|
|
52
102
|
const [compressIdle, setCompressIdle] = useState(true);
|
|
53
103
|
const [selection, setSelection] = useState<string | null>(null);
|
|
@@ -219,190 +269,147 @@ export function TraceView({ file, active, onBack }: TraceViewProps) {
|
|
|
219
269
|
);
|
|
220
270
|
const closeDrawer = useCallback(() => setDrawerOpen(false), []);
|
|
221
271
|
|
|
222
|
-
const traceStart = trace
|
|
223
|
-
|
|
224
|
-
const traceQuery = { ...query, data: trace };
|
|
272
|
+
const traceStart = trace.startedAt;
|
|
225
273
|
|
|
226
274
|
return (
|
|
227
|
-
<div className="stack traces-view">
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
<ArrowLeft size={14} aria-hidden="true" />
|
|
231
|
-
Sessions
|
|
232
|
-
</button>
|
|
233
|
-
</div>
|
|
234
|
-
<PageHeader
|
|
235
|
-
title={<span className="traces-title truncate">{trace?.title ?? file.split("/").pop()}</span>}
|
|
236
|
-
description={trace?.cwd ? <span className="mono">{trace.cwd}</span> : undefined}
|
|
237
|
-
actions={
|
|
238
|
-
<button
|
|
239
|
-
type="button"
|
|
240
|
-
className="btn"
|
|
241
|
-
data-variant="ghost"
|
|
242
|
-
data-icon="true"
|
|
243
|
-
onClick={query.refetch}
|
|
244
|
-
aria-label="Refresh trace"
|
|
245
|
-
title="Refresh trace"
|
|
246
|
-
>
|
|
247
|
-
<RefreshCw size={14} className={query.refreshing ? "traces-spin" : undefined} />
|
|
248
|
-
</button>
|
|
249
|
-
}
|
|
250
|
-
/>
|
|
275
|
+
<div className="stack traces-view" data-view={view}>
|
|
276
|
+
{view === "full" && <SummaryStrip summary={trace.summary} />}
|
|
277
|
+
{allSpans.length === 0 && <p className="muted">No spans recorded in this session.</p>}
|
|
251
278
|
|
|
252
|
-
<
|
|
253
|
-
{
|
|
279
|
+
<Card
|
|
280
|
+
index={1}
|
|
281
|
+
title="Timeline"
|
|
282
|
+
description="W/S zoom · A/D pan · drag pan · wheel zoom · 0 fit · F focus selection · dbl-click focus · Esc deselect"
|
|
283
|
+
actions={
|
|
254
284
|
<>
|
|
255
|
-
<
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
className="btn"
|
|
288
|
+
data-variant="ghost"
|
|
289
|
+
data-size="sm"
|
|
290
|
+
onClick={() => collapseAll(false)}
|
|
291
|
+
>
|
|
292
|
+
Expand all
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
type="button"
|
|
296
|
+
className="btn"
|
|
297
|
+
data-variant="ghost"
|
|
298
|
+
data-size="sm"
|
|
299
|
+
onClick={() => collapseAll(true)}
|
|
300
|
+
>
|
|
301
|
+
Collapse all
|
|
302
|
+
</button>
|
|
303
|
+
</>
|
|
304
|
+
}
|
|
305
|
+
>
|
|
306
|
+
<div className="stack">
|
|
307
|
+
<div className="traces-toolbar">
|
|
308
|
+
<Segmented options={MODE_OPTIONS} value={mode} onChange={setMode} size="sm" aria-label="Axis mode" />
|
|
309
|
+
{mode === "time" && (
|
|
310
|
+
<label className="traces-check">
|
|
311
|
+
<input
|
|
312
|
+
type="checkbox"
|
|
313
|
+
checked={compressIdle}
|
|
314
|
+
onChange={event => setCompressIdle(event.target.checked)}
|
|
315
|
+
/>
|
|
316
|
+
Compress idle
|
|
317
|
+
</label>
|
|
318
|
+
)}
|
|
319
|
+
<div className="traces-search">
|
|
320
|
+
<SearchInput
|
|
321
|
+
value={search}
|
|
322
|
+
onChange={value => {
|
|
323
|
+
setSearch(value);
|
|
324
|
+
setMatchIndex(0);
|
|
325
|
+
}}
|
|
326
|
+
label="Search spans"
|
|
327
|
+
placeholder="Search spans…"
|
|
328
|
+
width={200}
|
|
329
|
+
/>
|
|
330
|
+
{search.trim() && (
|
|
262
331
|
<>
|
|
332
|
+
<span className="num muted traces-match-count">
|
|
333
|
+
{matches.length === 0
|
|
334
|
+
? "0"
|
|
335
|
+
: `${(matchIndex % Math.max(matches.length, 1)) + 1}/${matches.length}`}
|
|
336
|
+
</span>
|
|
263
337
|
<button
|
|
264
338
|
type="button"
|
|
265
339
|
className="btn"
|
|
266
340
|
data-variant="ghost"
|
|
267
341
|
data-size="sm"
|
|
268
|
-
|
|
342
|
+
data-icon="true"
|
|
343
|
+
onClick={() => cycleMatch(-1)}
|
|
344
|
+
aria-label="Previous match"
|
|
345
|
+
disabled={matches.length === 0}
|
|
269
346
|
>
|
|
270
|
-
|
|
347
|
+
<ChevronLeft size={14} />
|
|
271
348
|
</button>
|
|
272
349
|
<button
|
|
273
350
|
type="button"
|
|
274
351
|
className="btn"
|
|
275
352
|
data-variant="ghost"
|
|
276
353
|
data-size="sm"
|
|
277
|
-
|
|
354
|
+
data-icon="true"
|
|
355
|
+
onClick={() => cycleMatch(1)}
|
|
356
|
+
aria-label="Next match"
|
|
357
|
+
disabled={matches.length === 0}
|
|
278
358
|
>
|
|
279
|
-
|
|
359
|
+
<ChevronRight size={14} />
|
|
280
360
|
</button>
|
|
281
361
|
</>
|
|
282
|
-
}
|
|
283
|
-
>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}}
|
|
310
|
-
placeholder="Search spans…"
|
|
311
|
-
width={200}
|
|
312
|
-
/>
|
|
313
|
-
{search.trim() && (
|
|
314
|
-
<>
|
|
315
|
-
<span className="num muted traces-match-count">
|
|
316
|
-
{matches.length === 0
|
|
317
|
-
? "0"
|
|
318
|
-
: `${(matchIndex % Math.max(matches.length, 1)) + 1}/${matches.length}`}
|
|
319
|
-
</span>
|
|
320
|
-
<button
|
|
321
|
-
type="button"
|
|
322
|
-
className="btn"
|
|
323
|
-
data-variant="ghost"
|
|
324
|
-
data-size="sm"
|
|
325
|
-
data-icon="true"
|
|
326
|
-
onClick={() => cycleMatch(-1)}
|
|
327
|
-
aria-label="Previous match"
|
|
328
|
-
disabled={matches.length === 0}
|
|
329
|
-
>
|
|
330
|
-
<ChevronLeft size={14} />
|
|
331
|
-
</button>
|
|
332
|
-
<button
|
|
333
|
-
type="button"
|
|
334
|
-
className="btn"
|
|
335
|
-
data-variant="ghost"
|
|
336
|
-
data-size="sm"
|
|
337
|
-
data-icon="true"
|
|
338
|
-
onClick={() => cycleMatch(1)}
|
|
339
|
-
aria-label="Next match"
|
|
340
|
-
disabled={matches.length === 0}
|
|
341
|
-
>
|
|
342
|
-
<ChevronRight size={14} />
|
|
343
|
-
</button>
|
|
344
|
-
</>
|
|
345
|
-
)}
|
|
346
|
-
</div>
|
|
347
|
-
<div className="traces-legend">
|
|
348
|
-
{LEGEND.map(item => (
|
|
349
|
-
<span key={item.kind} className="traces-legend-item">
|
|
350
|
-
<span
|
|
351
|
-
className="swatch"
|
|
352
|
-
style={{ background: `var(${CATEGORY_VARS[item.kind]})` }}
|
|
353
|
-
/>
|
|
354
|
-
{item.label}
|
|
355
|
-
</span>
|
|
356
|
-
))}
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
|
-
<Minimap
|
|
360
|
-
tracks={tracks}
|
|
361
|
-
scale={scale}
|
|
362
|
-
viewport={effectiveViewport}
|
|
363
|
-
onViewportChange={setViewport}
|
|
364
|
-
/>
|
|
365
|
-
<TimelineCanvas
|
|
366
|
-
tracks={tracks}
|
|
367
|
-
scale={scale}
|
|
368
|
-
viewport={effectiveViewport}
|
|
369
|
-
onViewportChange={setViewport}
|
|
370
|
-
selected={selected}
|
|
371
|
-
onSelect={selectAndOpen}
|
|
372
|
-
matchIds={matchIds}
|
|
373
|
-
collapsed={collapsed}
|
|
374
|
-
onToggleCollapse={toggleCollapse}
|
|
375
|
-
traceStart={traceStart}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
</Card>
|
|
379
|
-
|
|
380
|
-
<Card
|
|
381
|
-
index={2}
|
|
382
|
-
title="Transcript"
|
|
383
|
-
description="Every span and marker in order; click to inspect"
|
|
384
|
-
flush
|
|
385
|
-
>
|
|
386
|
-
<TranscriptList
|
|
387
|
-
tracks={tracks}
|
|
388
|
-
selection={selection}
|
|
389
|
-
onSelect={openFromTranscript}
|
|
390
|
-
search={search}
|
|
391
|
-
matchIds={matchIds}
|
|
392
|
-
traceStart={traceStart}
|
|
393
|
-
/>
|
|
394
|
-
</Card>
|
|
395
|
-
<AggregatesPanel toolStats={loaded.summary.toolStats} index={3} />
|
|
396
|
-
</>
|
|
397
|
-
)}
|
|
398
|
-
</QueryView>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
<div className="traces-legend">
|
|
365
|
+
{LEGEND.map(item => (
|
|
366
|
+
<span key={item.kind} className="traces-legend-item">
|
|
367
|
+
<span className="swatch" style={{ background: `var(${CATEGORY_VARS[item.kind]})` }} />
|
|
368
|
+
{item.label}
|
|
369
|
+
</span>
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<Minimap tracks={tracks} scale={scale} viewport={effectiveViewport} onViewportChange={setViewport} />
|
|
374
|
+
<TimelineCanvas
|
|
375
|
+
tracks={tracks}
|
|
376
|
+
scale={scale}
|
|
377
|
+
viewport={effectiveViewport}
|
|
378
|
+
onViewportChange={setViewport}
|
|
379
|
+
selected={selected}
|
|
380
|
+
onSelect={selectAndOpen}
|
|
381
|
+
matchIds={matchIds}
|
|
382
|
+
collapsed={collapsed}
|
|
383
|
+
onToggleCollapse={toggleCollapse}
|
|
384
|
+
traceStart={traceStart}
|
|
385
|
+
autoFocus={!embedded}
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
</Card>
|
|
399
389
|
|
|
400
390
|
<SpanDrawer
|
|
401
391
|
span={drawerOpen ? (selected?.span ?? null) : null}
|
|
402
392
|
track={selected?.track ?? null}
|
|
403
393
|
onClose={closeDrawer}
|
|
404
394
|
onOpenChildTrack={openChildTrack}
|
|
395
|
+
loadEntry={loadEntry}
|
|
396
|
+
inline={embedded}
|
|
397
|
+
onNavigate={onNavigate}
|
|
405
398
|
/>
|
|
399
|
+
|
|
400
|
+
{view === "full" && (
|
|
401
|
+
<Card index={2} title="Transcript" description="Every span and marker in order; click to inspect" flush>
|
|
402
|
+
<TranscriptList
|
|
403
|
+
tracks={tracks}
|
|
404
|
+
selection={selection}
|
|
405
|
+
onSelect={openFromTranscript}
|
|
406
|
+
search={search}
|
|
407
|
+
matchIds={matchIds}
|
|
408
|
+
traceStart={traceStart}
|
|
409
|
+
/>
|
|
410
|
+
</Card>
|
|
411
|
+
)}
|
|
412
|
+
{view === "full" && <AggregatesPanel toolStats={trace.summary.toolStats} index={3} />}
|
|
406
413
|
</div>
|
|
407
414
|
);
|
|
408
415
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Shared trace UI for live dashboards and embedded offline viewers. */
|
|
2
|
+
export type { SessionTrace } from "../types";
|
|
3
|
+
export type { TraceEntryLoader } from "./SpanDrawer";
|
|
4
|
+
export { SessionTraceView, type SessionTraceViewProps } from "./TraceView";
|
|
5
|
+
export { TraceThemeRoot } from "./trace-colors";
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* design tokens so canvas drawing follows the active theme.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
7
7
|
import type { TraceSpanKind } from "../types";
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
/** Embedded viewers resolve canvas colors from their scoped style root. */
|
|
10
|
+
export const TraceThemeRoot = createContext<HTMLElement | null>(null);
|
|
9
11
|
|
|
10
12
|
export interface TraceTheme {
|
|
11
13
|
/** Span fill per category. */
|
|
@@ -42,8 +44,8 @@ export const CATEGORY_VARS: Record<TraceSpanKind, string> = {
|
|
|
42
44
|
background: "--ink-4",
|
|
43
45
|
};
|
|
44
46
|
|
|
45
|
-
function readTheme(): TraceTheme {
|
|
46
|
-
const style = getComputedStyle(
|
|
47
|
+
function readTheme(root: HTMLElement): TraceTheme {
|
|
48
|
+
const style = getComputedStyle(root);
|
|
47
49
|
const v = (name: string, fallback: string) => style.getPropertyValue(name).trim() || fallback;
|
|
48
50
|
return {
|
|
49
51
|
category: {
|
|
@@ -69,16 +71,18 @@ function readTheme(): TraceTheme {
|
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* Current trace palette. Re-reads the tokens whenever the resolved theme
|
|
72
|
-
* changes
|
|
74
|
+
* changes, without loading the live dashboard's theme preference store.
|
|
73
75
|
*/
|
|
74
76
|
export function useTraceTheme(): TraceTheme {
|
|
75
|
-
const
|
|
77
|
+
const root = useContext(TraceThemeRoot) ?? document.documentElement;
|
|
76
78
|
const [attrVersion, setAttrVersion] = useState(0);
|
|
77
79
|
useEffect(() => {
|
|
78
80
|
const observer = new MutationObserver(() => setAttrVersion(n => n + 1));
|
|
79
|
-
|
|
81
|
+
const options = { attributes: true, attributeFilter: ["data-theme", "style", "class"] };
|
|
82
|
+
observer.observe(document.documentElement, options);
|
|
83
|
+
if (root !== document.documentElement) observer.observe(root, options);
|
|
80
84
|
return () => observer.disconnect();
|
|
81
|
-
}, []);
|
|
82
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies:
|
|
83
|
-
return useMemo(readTheme, [
|
|
85
|
+
}, [root]);
|
|
86
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: attrVersion triggers a CSS token re-read.
|
|
87
|
+
return useMemo(() => readTheme(root), [root, attrVersion]);
|
|
84
88
|
}
|
|
@@ -4,17 +4,19 @@ export interface SearchInputProps {
|
|
|
4
4
|
value: string;
|
|
5
5
|
onChange: (value: string) => void;
|
|
6
6
|
placeholder?: string;
|
|
7
|
+
label?: string;
|
|
7
8
|
width?: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/** Text filter with a leading search glyph. */
|
|
11
|
-
export function SearchInput({ value, onChange, placeholder = "Filter…", width }: SearchInputProps) {
|
|
12
|
+
export function SearchInput({ value, onChange, placeholder = "Filter…", label = "Filter", width }: SearchInputProps) {
|
|
12
13
|
return (
|
|
13
14
|
<label className="search">
|
|
14
15
|
<Search size={14} />
|
|
15
16
|
<input
|
|
16
17
|
className="input"
|
|
17
18
|
type="search"
|
|
19
|
+
aria-label={label}
|
|
18
20
|
value={value}
|
|
19
21
|
placeholder={placeholder}
|
|
20
22
|
onChange={e => onChange(e.target.value)}
|