@telorun/debug-ui 0.1.0 → 0.2.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/src/index.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  // Browser-safe, framework-agnostic surface: wire types + filtering + SSE client.
2
2
  // No React, no Node built-ins — importable by the CLI (bun `source` condition),
3
- // the editor webview, and the standalone app alike.
3
+ // the editor webview, and the standalone app alike. The wire types re-export
4
+ // from `@telorun/debug-wire` (the shared contract).
4
5
  export {
6
+ type DebugFrame,
5
7
  type DebugEvent,
8
+ type DebugLog,
6
9
  type WireRef,
7
10
  type WireBlob,
11
+ isLogFrame,
12
+ isEventFrame,
8
13
  isWireRef,
9
14
  isWireBlob,
10
15
  eventSuffix,
11
16
  } from "./wire.js";
17
+ export { type AppEndpoint, endpointHref, endpointLabel } from "./endpoints.js";
12
18
  export { type EventFilter, matchesFilter, distinctSuffixes } from "./filter.js";
19
+ export type { DebugTheme } from "./theme.js";
13
20
  export { type DebugStreamHandlers, connectDebugStream } from "./sse-client.js";
14
21
  export { type FoundBlob, collectBlobs, blobDimensions, formatBytes } from "./media.js";
package/src/sse-client.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { DebugEvent } from "./wire.js";
1
+ import type { DebugFrame } from "./wire.js";
2
2
 
3
3
  export interface DebugStreamHandlers {
4
- onEvent: (event: DebugEvent) => void;
4
+ /** One frame off the stream — a kernel event or a log line (route on `kind`). */
5
+ onFrame: (frame: DebugFrame) => void;
5
6
  /** Connection state changes — drives a status indicator. */
6
7
  onStatus?: (status: "connecting" | "open" | "closed") => void;
7
8
  }
@@ -24,7 +25,7 @@ export function connectDebugStream(url: string, handlers: DebugStreamHandlers):
24
25
  source.onmessage = (msg) => {
25
26
  if (!msg.data) return;
26
27
  try {
27
- handlers.onEvent(JSON.parse(msg.data) as DebugEvent);
28
+ handlers.onFrame(JSON.parse(msg.data) as DebugFrame);
28
29
  } catch {
29
30
  // A malformed frame shouldn't kill the stream; skip it.
30
31
  }
package/src/theme.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ /** Requested color theme. `"system"` (the default) follows the OS preference. */
4
+ export type DebugTheme = "light" | "dark" | "system";
5
+
6
+ const MEDIA = "(prefers-color-scheme: dark)";
7
+ const STORAGE_KEY = "telo-debug-ui:theme";
8
+
9
+ /** The persisted standalone theme choice, or `"system"`. Used when the panel
10
+ * owns its mode (no host-supplied `theme`) so the toggle survives a reload. */
11
+ export function loadStoredTheme(): DebugTheme {
12
+ try {
13
+ const raw = window.localStorage.getItem(STORAGE_KEY);
14
+ if (raw === "light" || raw === "dark" || raw === "system") return raw;
15
+ } catch {
16
+ // localStorage may be unavailable — fall back to system.
17
+ }
18
+ return "system";
19
+ }
20
+
21
+ export function storeTheme(theme: DebugTheme): void {
22
+ try {
23
+ window.localStorage.setItem(STORAGE_KEY, theme);
24
+ } catch {
25
+ // Non-fatal — the choice just won't persist across reloads.
26
+ }
27
+ }
28
+
29
+ function systemTheme(): "light" | "dark" {
30
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
31
+ return window.matchMedia(MEDIA).matches ? "dark" : "light";
32
+ }
33
+ return "dark";
34
+ }
35
+
36
+ /** Resolve a {@link DebugTheme} to a concrete `"light"` / `"dark"`. When `theme`
37
+ * is `"system"` (or omitted) it tracks the OS preference and updates live; an
38
+ * explicit `"light"` / `"dark"` (e.g. passed by an embedding host) wins. */
39
+ export function useResolvedTheme(theme: DebugTheme = "system"): "light" | "dark" {
40
+ const [system, setSystem] = useState<"light" | "dark">(systemTheme);
41
+ useEffect(() => {
42
+ if (theme !== "system") return;
43
+ const mq = window.matchMedia(MEDIA);
44
+ const onChange = (): void => setSystem(mq.matches ? "dark" : "light");
45
+ onChange();
46
+ mq.addEventListener("change", onChange);
47
+ return () => mq.removeEventListener("change", onChange);
48
+ }, [theme]);
49
+ return theme === "system" ? system : theme;
50
+ }
package/src/wire.ts CHANGED
@@ -1,77 +1,4 @@
1
- /**
2
- * The Telo debug event wire format — the language-neutral contract between a
3
- * kernel runtime (the producer: Node today, Rust/Go later) and any consumer
4
- * (this UI, the editor). A producer serializes each emitted event to this shape;
5
- * consumers only ever parse it. The companion JSON Schema (`wire-schema.json`)
6
- * is the source of truth a non-TypeScript producer conforms to.
7
- */
8
-
9
- /** One event as it travels over the wire (one JSON object per SSE message / JSONL line). */
10
- export interface DebugEvent {
11
- /** ISO-8601 timestamp set by the producer when the event was emitted. */
12
- timestamp: string;
13
- /** Dotted event name, e.g. `Server.Listening`, `MyKind.MyName.Invoked`. */
14
- event: string;
15
- /** Arbitrary event payload, already reduced to wire-safe values (see encoding rules). */
16
- payload?: unknown;
17
- /** Optional producer-attached metadata (namespace, resource, kind, name, …). */
18
- metadata?: Record<string, unknown>;
19
- }
20
-
21
- /**
22
- * Value-encoding rules a producer applies before a value enters `payload`:
23
- * - a resolved `!ref` (a live resource instance) → `{ kind, name }`
24
- * - any other live / unrepresentable value (controller instance, stream, client,
25
- * function, bigint) → a `"[Marker]"` string (e.g. `"[Stream]"`, `"[Kernel]"`)
26
- * - a reference cycle → `"[Circular]"`
27
- * - everything else → plain JSON
28
- * Consumers shouldn't assume `payload` is any particular shape.
29
- */
30
-
31
- /** A resolved-reference value as it appears in a payload. */
32
- export interface WireRef {
33
- kind: string;
34
- name: string;
35
- }
36
-
37
- /**
38
- * A binary value offloaded to the producer's blob store. The bytes are NOT in the
39
- * payload — `$blob` is a path (relative to the producer origin) the consumer
40
- * fetches on demand (e.g. `<img src>`, a download link). The object key this sits
41
- * under is preserved; only the bytes left the log.
42
- */
43
- export interface WireBlob {
44
- $blob: string;
45
- mediaType: string;
46
- byteLength: number;
47
- }
48
-
49
- /** Narrow a payload value to a {@link WireBlob}. */
50
- export function isWireBlob(value: unknown): value is WireBlob {
51
- return (
52
- !!value &&
53
- typeof value === "object" &&
54
- !Array.isArray(value) &&
55
- typeof (value as WireBlob).$blob === "string" &&
56
- typeof (value as WireBlob).mediaType === "string"
57
- );
58
- }
59
-
60
- /** Narrow a payload value to a {@link WireRef} (e.g. to render it as a chip/link). */
61
- export function isWireRef(value: unknown): value is WireRef {
62
- return (
63
- !!value &&
64
- typeof value === "object" &&
65
- !Array.isArray(value) &&
66
- typeof (value as WireRef).kind === "string" &&
67
- typeof (value as WireRef).name === "string" &&
68
- Object.keys(value as object).length === 2
69
- );
70
- }
71
-
72
- /** The trailing segment of a dotted event name — the "kind" of event
73
- * (`Invoked`, `Failed`, `Listening`, …). Used for facet filtering and color. */
74
- export function eventSuffix(event: string): string {
75
- const dot = event.lastIndexOf(".");
76
- return dot >= 0 ? event.slice(dot + 1) : event;
77
- }
1
+ // The wire format now lives in `@telorun/debug-wire` (the language-neutral
2
+ // frame contract shared by the producer, runner, editor, and this UI). This
3
+ // module re-exports it so the UI's internal `./wire.js` imports keep working.
4
+ export * from "@telorun/debug-wire";
@@ -1 +0,0 @@
1
- .tdbg-root{--tdbg-bg: #0f1115;--tdbg-panel: #161a21;--tdbg-line: #232a34;--tdbg-text: #d7dce3;--tdbg-muted: #7a8493;--tdbg-accent: #4f9cf0;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-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-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:#fff;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:#f8514933;color:#ff8079}.tdbg-suffix-invoked,.tdbg-suffix-listening,.tdbg-suffix-started,.tdbg-suffix-initialized{background:#3fb9502e;color:#5fd673}.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)}