@narumitw/pi-inspect 0.0.0-stage → 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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/dist/app.css +1 -0
  4. package/dist/app.js +128 -0
  5. package/dist/index.html +1 -0
  6. package/docs/REVIEW.md +189 -0
  7. package/docs/VERIFICATION.md +186 -0
  8. package/docs/viewer.md +74 -0
  9. package/package.json +67 -4
  10. package/src/ancestry.ts +32 -0
  11. package/src/collector.ts +143 -0
  12. package/src/context.ts +115 -0
  13. package/src/correlation.ts +27 -0
  14. package/src/entry-index.ts +17 -0
  15. package/src/entry-summary.ts +34 -0
  16. package/src/extension.ts +259 -0
  17. package/src/feed.ts +145 -0
  18. package/src/identity.ts +112 -0
  19. package/src/index.ts +1 -0
  20. package/src/model.ts +117 -0
  21. package/src/native-projection.ts +28 -0
  22. package/src/privacy.ts +136 -0
  23. package/src/projection.ts +374 -0
  24. package/src/server.ts +202 -0
  25. package/src/system-message.ts +117 -0
  26. package/src/web/api.ts +137 -0
  27. package/src/web/app.tsx +570 -0
  28. package/src/web/captured-executions.tsx +177 -0
  29. package/src/web/components.css +261 -0
  30. package/src/web/components.tsx +241 -0
  31. package/src/web/composition.css +554 -0
  32. package/src/web/context-composition.tsx +329 -0
  33. package/src/web/context-inventory.tsx +65 -0
  34. package/src/web/entry-calls.ts +14 -0
  35. package/src/web/format.ts +94 -0
  36. package/src/web/hierarchy.ts +117 -0
  37. package/src/web/history.ts +56 -0
  38. package/src/web/inline-entry.tsx +124 -0
  39. package/src/web/inspector.tsx +386 -0
  40. package/src/web/json-tree.tsx +74 -0
  41. package/src/web/overview.tsx +67 -0
  42. package/src/web/panels.css +267 -0
  43. package/src/web/panes.tsx +123 -0
  44. package/src/web/recorded-content.tsx +49 -0
  45. package/src/web/search.ts +16 -0
  46. package/src/web/session-filters.tsx +100 -0
  47. package/src/web/session.css +114 -0
  48. package/src/web/style.css +332 -0
  49. package/src/web/styles.d.ts +1 -0
  50. package/src/web/timeline.tsx +33 -0
  51. package/src/web/timing.ts +26 -0
  52. package/src/web/trace.css +411 -0
  53. package/src/web/trace.tsx +313 -0
  54. package/src/web/virtual-context.ts +138 -0
@@ -0,0 +1,267 @@
1
+ .sidebar {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ min-height: 0;
6
+ gap: 5px;
7
+ }
8
+ .tree-panel {
9
+ display: flex;
10
+ flex-direction: column;
11
+ flex: 1;
12
+ min-height: 0;
13
+ }
14
+ .tree-controls {
15
+ padding: 0 9px 8px;
16
+ }
17
+ .tree-controls .rt-TextFieldRoot {
18
+ background: var(--panel-raised);
19
+ box-shadow: none;
20
+ border: 1px solid var(--panel-border);
21
+ font-size: 11px;
22
+ }
23
+ .filter-pills {
24
+ display: flex;
25
+ gap: 4px;
26
+ margin-top: 7px;
27
+ }
28
+ .filter-pills button {
29
+ flex: 1;
30
+ padding: 4px;
31
+ border: 0;
32
+ border-radius: 4px;
33
+ background: transparent;
34
+ color: var(--secondary);
35
+ font-size: 11px;
36
+ }
37
+ .filter-pills button[aria-pressed="true"] {
38
+ background: var(--selected-bg);
39
+ color: var(--blue-11);
40
+ }
41
+ .tree-scroll {
42
+ flex: 1;
43
+ min-height: 0;
44
+ overflow: auto;
45
+ padding: 2px 5px;
46
+ }
47
+ .tree-row {
48
+ display: flex;
49
+ align-items: center;
50
+ min-width: 0;
51
+ gap: 2px;
52
+ }
53
+ .tree-row .rt-IconButton {
54
+ width: 18px;
55
+ height: 22px;
56
+ flex-shrink: 0;
57
+ color: var(--muted);
58
+ }
59
+ .tree-spacer {
60
+ width: 18px;
61
+ flex-shrink: 0;
62
+ }
63
+ .node {
64
+ flex: 1;
65
+ min-width: 0;
66
+ display: flex;
67
+ gap: 7px;
68
+ align-items: center;
69
+ padding: 5px 4px;
70
+ border: 0;
71
+ border-radius: 4px;
72
+ background: transparent;
73
+ color: var(--gray-12);
74
+ text-align: left;
75
+ }
76
+ .node:hover {
77
+ background: var(--surface-hover);
78
+ }
79
+ .node[aria-current="true"] {
80
+ background: var(--selected-bg);
81
+ box-shadow: inset 2px 0 var(--blue-9);
82
+ }
83
+ .node-copy {
84
+ min-width: 0;
85
+ flex: 1;
86
+ display: block;
87
+ }
88
+ .node-title {
89
+ display: flex;
90
+ align-items: baseline;
91
+ gap: 6px;
92
+ min-width: 0;
93
+ white-space: nowrap;
94
+ }
95
+ .node-title strong {
96
+ font-size: 11px;
97
+ font-weight: 600;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ }
101
+ .node-title small {
102
+ font:
103
+ 10px / 1.4 Consolas,
104
+ monospace;
105
+ color: var(--muted);
106
+ flex-shrink: 0;
107
+ }
108
+ .node-description {
109
+ display: block;
110
+ white-space: nowrap;
111
+ overflow: hidden;
112
+ text-overflow: ellipsis;
113
+ color: var(--secondary);
114
+ font-size: 10px;
115
+ line-height: 15px;
116
+ }
117
+ .tree-count,
118
+ .nav-pagination {
119
+ border-top: 1px solid var(--panel-border);
120
+ padding: 6px;
121
+ color: var(--muted);
122
+ text-align: center;
123
+ font-size: 10px;
124
+ }
125
+ .nav-pagination {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ gap: 4px;
129
+ }
130
+ .nav-pagination button {
131
+ background: none;
132
+ border: 0;
133
+ color: var(--secondary);
134
+ font-size: 10px;
135
+ }
136
+ .filter-panel {
137
+ flex-shrink: 0;
138
+ padding: 0 9px;
139
+ }
140
+ .filters-toggle {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ width: 100%;
145
+ gap: 6px;
146
+ background: none;
147
+ border: 0;
148
+ color: var(--secondary);
149
+ padding: 8px 0;
150
+ font-size: 12px;
151
+ }
152
+ .filters-toggle svg {
153
+ width: 14px;
154
+ }
155
+ .filter-check {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+ margin: 8px 0;
160
+ font-size: 11px;
161
+ color: var(--secondary);
162
+ cursor: pointer;
163
+ }
164
+ .filter-panel .rt-Flex {
165
+ padding-bottom: 9px;
166
+ }
167
+ .filter-panel .rt-SelectTrigger {
168
+ max-width: 135px;
169
+ font-size: 11px;
170
+ }
171
+ .inspector-panel {
172
+ display: flex;
173
+ flex-direction: column;
174
+ height: 100%;
175
+ min-height: 0;
176
+ }
177
+ .inspector-identity {
178
+ padding: 2px 10px 8px;
179
+ }
180
+ .inspector-identity .rt-Heading {
181
+ font-size: 14px;
182
+ line-height: 20px;
183
+ overflow-wrap: anywhere;
184
+ }
185
+ .inspector-identity .rt-Text {
186
+ font-size: 11px;
187
+ line-height: 16px;
188
+ }
189
+ .inspector-tabs {
190
+ flex: 1;
191
+ min-height: 0;
192
+ overflow-y: auto;
193
+ overflow-x: hidden;
194
+ padding: 0 8px 8px;
195
+ }
196
+ .inspector-tabs > .rt-TabsList {
197
+ display: flex;
198
+ flex-wrap: nowrap;
199
+ overflow-x: auto;
200
+ position: sticky;
201
+ top: 0;
202
+ z-index: 2;
203
+ background: var(--panel-bg);
204
+ margin-bottom: 6px;
205
+ flex-shrink: 0;
206
+ }
207
+ .inspector-tabs .rt-TabsTrigger {
208
+ padding: 0 7px;
209
+ height: 31px;
210
+ font-size: 11px;
211
+ white-space: nowrap;
212
+ flex-shrink: 0;
213
+ text-transform: capitalize;
214
+ }
215
+ .inspector-tabs .rt-TabsTriggerInner,
216
+ .inspector-tabs .rt-TabsTriggerInnerHidden {
217
+ padding: 0;
218
+ }
219
+ .inspector-tabs .rt-TabsContent {
220
+ padding-top: 0;
221
+ }
222
+ .format-toolbar {
223
+ display: flex;
224
+ justify-content: end;
225
+ padding: 3px 0 6px;
226
+ }
227
+ .inspector-metadata {
228
+ padding: 0 3px 5px;
229
+ }
230
+ .inspector-metadata .metadata > div {
231
+ border-bottom: 1px solid var(--panel-border);
232
+ }
233
+ .related {
234
+ border-top: 1px solid var(--panel-border);
235
+ padding: 5px 0;
236
+ margin-top: 8px;
237
+ }
238
+ .related > .rt-Button {
239
+ width: 100%;
240
+ justify-content: space-between;
241
+ }
242
+ .related button:not(.rt-Button) {
243
+ display: block;
244
+ padding: 7px 4px;
245
+ width: 100%;
246
+ text-align: left;
247
+ background: none;
248
+ border: 0;
249
+ color: var(--secondary);
250
+ font-size: 11px;
251
+ }
252
+ .inventory-card {
253
+ background: transparent;
254
+ border: 0;
255
+ border-bottom: 1px solid var(--panel-border);
256
+ border-radius: 0;
257
+ box-shadow: none;
258
+ margin: 6px 0;
259
+ padding: 9px 2px;
260
+ }
261
+ .inventory-card .rt-Flex {
262
+ margin: 5px 0;
263
+ }
264
+ .inventory-card .rt-Text {
265
+ overflow-wrap: anywhere;
266
+ font-size: 11px;
267
+ }
@@ -0,0 +1,123 @@
1
+ import { Cross2Icon } from "@radix-ui/react-icons";
2
+ import { Dialog, IconButton } from "@radix-ui/themes";
3
+ import { type ReactNode, type RefObject, useEffect, useState } from "react";
4
+
5
+ export function useNarrow(): boolean {
6
+ const [width, setWidth] = useState(() => window.innerWidth);
7
+ useEffect(() => {
8
+ const changed = () => setWidth(window.innerWidth);
9
+ window.addEventListener("resize", changed);
10
+ return () => window.removeEventListener("resize", changed);
11
+ }, []);
12
+ return width <= 1100;
13
+ }
14
+ export function PaneDrawer({
15
+ name,
16
+ open,
17
+ change,
18
+ children,
19
+ side,
20
+ trigger,
21
+ }: {
22
+ name: string;
23
+ open: boolean;
24
+ change(open: boolean): void;
25
+ children: ReactNode;
26
+ side: string;
27
+ trigger: RefObject<HTMLButtonElement | null>;
28
+ }) {
29
+ return (
30
+ <Dialog.Root open={open} onOpenChange={change}>
31
+ <Dialog.Content
32
+ className={`side-drawer side-drawer-${side}`}
33
+ onCloseAutoFocus={(event) => {
34
+ event.preventDefault();
35
+ if (trigger.current?.isConnected) trigger.current.focus({ preventScroll: true });
36
+ }}
37
+ >
38
+ <div className="drawer-title">
39
+ <Dialog.Title>{name}</Dialog.Title>
40
+ <Dialog.Close>
41
+ <IconButton aria-label={`Close ${name}`} variant="ghost">
42
+ <Cross2Icon />
43
+ </IconButton>
44
+ </Dialog.Close>
45
+ </div>
46
+ <Dialog.Description className="sr-only">Read-only browser panel. Escape closes this drawer.</Dialog.Description>
47
+ {children}
48
+ </Dialog.Content>
49
+ </Dialog.Root>
50
+ );
51
+ }
52
+ export function ResizeHandle({
53
+ name,
54
+ value,
55
+ change,
56
+ min,
57
+ max,
58
+ inverse = false,
59
+ }: {
60
+ name: string;
61
+ value: number;
62
+ change(value: number): void;
63
+ min: number;
64
+ max: number;
65
+ inverse?: boolean;
66
+ }) {
67
+ const [dragging, setDragging] = useState(false);
68
+ const [start, setStart] = useState({ x: 0, value: 0 });
69
+ useEffect(() => {
70
+ if (!dragging) return;
71
+ const controller = new AbortController();
72
+ const move = (event: PointerEvent) =>
73
+ change(Math.max(min, Math.min(max, start.value + (event.clientX - start.x) * (inverse ? -1 : 1))));
74
+ const end = () => setDragging(false);
75
+ window.addEventListener("pointermove", move, { signal: controller.signal });
76
+ window.addEventListener("pointerup", end, { signal: controller.signal });
77
+ window.addEventListener("pointercancel", end, { signal: controller.signal });
78
+ window.addEventListener("blur", end, { signal: controller.signal });
79
+ window.addEventListener(
80
+ "keydown",
81
+ (event) => {
82
+ if (event.key === "Escape") {
83
+ event.preventDefault();
84
+ end();
85
+ }
86
+ },
87
+ { signal: controller.signal },
88
+ );
89
+ return () => controller.abort();
90
+ }, [dragging, start, change, min, max, inverse]);
91
+ return (
92
+ <hr
93
+ aria-label={name}
94
+ aria-orientation="vertical"
95
+ aria-valuenow={Math.round(value)}
96
+ aria-valuemin={min}
97
+ aria-valuemax={max}
98
+ tabIndex={0}
99
+ className="pane-resizer"
100
+ onPointerDown={(event) => {
101
+ if (event.button !== 0) return;
102
+ event.preventDefault();
103
+ setStart({ x: event.clientX, value });
104
+ setDragging(true);
105
+ }}
106
+ onKeyDown={(event) => {
107
+ if (event.key === "Escape") {
108
+ setDragging(false);
109
+ return;
110
+ }
111
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
112
+ event.preventDefault();
113
+ change(
114
+ event.key === "Home"
115
+ ? min
116
+ : event.key === "End"
117
+ ? max
118
+ : Math.max(min, Math.min(max, value + (event.key === "ArrowRight" ? 10 : -10) * (inverse ? -1 : 1))),
119
+ );
120
+ }}
121
+ />
122
+ );
123
+ }
@@ -0,0 +1,49 @@
1
+ import type { Capture } from "../model.js";
2
+ import { Data } from "./components.js";
3
+ import { record } from "./format.js";
4
+
5
+ export function RecordedContent({ data, scope }: { data?: Capture; scope: string }) {
6
+ const raw = record(data?.value);
7
+ const message = record(raw?.message);
8
+ const content =
9
+ message && Object.hasOwn(message, "content")
10
+ ? message.content
11
+ : raw && Object.hasOwn(raw, "data")
12
+ ? raw.data
13
+ : (raw?.content ?? raw?.summary ?? raw);
14
+ const blocks = Array.isArray(content) ? content : [content];
15
+ return (
16
+ <div className="recorded-content">
17
+ {blocks.slice(0, 128).map((block, index) => {
18
+ const item = record(block);
19
+ const name = typeof item?.name === "string" ? item.name : "unknown";
20
+ const label =
21
+ item?.type === "thinking"
22
+ ? "Thinking"
23
+ : item?.type === "toolCall"
24
+ ? `Tool call · ${name}`
25
+ : "Recorded content";
26
+ return (
27
+ <Data
28
+ // biome-ignore lint/suspicious/noArrayIndexKey: Historical content blocks are immutable and scoped by entry identity.
29
+ key={`${scope}-${index}`}
30
+ label={label}
31
+ data={
32
+ data && {
33
+ value: item?.text ?? item?.thinking ?? item?.arguments ?? block ?? null,
34
+ truncated: data.truncated || blocks.length > 128,
35
+ }
36
+ }
37
+ scope={`${scope}-content-${index}`}
38
+ />
39
+ );
40
+ })}
41
+ <Data
42
+ label="Metadata and Raw JSON · redacted display copy"
43
+ data={data}
44
+ scope={`${scope}-raw`}
45
+ defaultOpen={false}
46
+ />
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,16 @@
1
+ // Display-only browser ingestion. Strip complete terminal sequences before bounding the query.
2
+ export function boundedSearch(value: string): string {
3
+ return (
4
+ value
5
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional terminal-control sanitation.
6
+ .replace(/(?:\x1b[\]^_P]|[\x90\x9d\x9e\x9f])[\s\S]*?(?:\x07|\x1b\\|\x9c|$)/g, "")
7
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Intentional CSI and escape sanitation.
8
+ .replace(/(?:\x1b\[|\x9b)[0-?]*[ -/]*[@-~]|\x1b[ -/]*[@-~]/g, "")
9
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Search contains no terminal controls.
10
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, "")
11
+ .slice(0, 512)
12
+ );
13
+ }
14
+ export function searchNeedle(value: string): string {
15
+ return boundedSearch(value).toLowerCase();
16
+ }
@@ -0,0 +1,100 @@
1
+ import { Checkbox, Heading, Select, TextField } from "@radix-ui/themes";
2
+ import { useId } from "react";
3
+ import type { Filters } from "./format.js";
4
+
5
+ /** Filters only: ancestry lives in the primary Branch view, never a duplicate sidebar. */
6
+ export function SessionFilters({ filters, change }: { filters: Filters; change(filters: Filters): void }) {
7
+ const id = useId();
8
+ return (
9
+ <aside className="sidebar panel session-filters">
10
+ <div className="panel-heading">
11
+ <Heading size="3">Session filters</Heading>
12
+ </div>
13
+ <TextField.Root
14
+ aria-label="Search session"
15
+ placeholder="Search entries or content…"
16
+ value={filters.query}
17
+ onChange={(event) => change({ ...filters, query: event.target.value })}
18
+ />
19
+ <div className="filter-pills">
20
+ {["All", "Model", "Tool", "Custom"].map((group) => (
21
+ <button
22
+ type="button"
23
+ key={group}
24
+ aria-pressed={
25
+ group === "All" ? !filters.groups.length : filters.groups.length === 1 && filters.groups[0] === group
26
+ }
27
+ onClick={() => change({ ...filters, kind: "all", groups: group === "All" ? [] : [group] })}
28
+ >
29
+ {group}
30
+ </button>
31
+ ))}
32
+ </div>
33
+ <details className="filter-panel">
34
+ <summary>Filters</summary>
35
+ <label className="filter-check" htmlFor={`${id}-errors`}>
36
+ <span>Errors only</span>
37
+ <Checkbox
38
+ id={`${id}-errors`}
39
+ checked={filters.errorsOnly}
40
+ onCheckedChange={(checked) => change({ ...filters, errorsOnly: checked === true })}
41
+ />
42
+ </label>
43
+ <label className="filter-check" htmlFor={`${id}-slow`}>
44
+ <span>Slow tool calls (&gt; 10s)</span>
45
+ <Checkbox
46
+ id={`${id}-slow`}
47
+ checked={filters.slowOnly}
48
+ onCheckedChange={(checked) => change({ ...filters, slowOnly: checked === true })}
49
+ />
50
+ </label>
51
+ {["Tool", "Model", "Custom"].map((group) => (
52
+ <label className="filter-check" key={group} htmlFor={`${id}-${group}`}>
53
+ <span>{group} events</span>
54
+ <Checkbox
55
+ id={`${id}-${group}`}
56
+ checked={filters.groups.includes(group)}
57
+ onCheckedChange={() =>
58
+ change({
59
+ ...filters,
60
+ groups: filters.groups.includes(group)
61
+ ? filters.groups.filter((item) => item !== group)
62
+ : [...filters.groups, group],
63
+ })
64
+ }
65
+ />
66
+ </label>
67
+ ))}
68
+ <label className="filter-check" htmlFor={`${id}-kind`}>
69
+ <span>Entry type</span>
70
+ <Select.Root value={filters.kind} onValueChange={(kind) => change({ ...filters, kind })}>
71
+ <Select.Trigger id={`${id}-kind`} aria-label="Filter entry type" />
72
+ <Select.Content>
73
+ {[
74
+ "all",
75
+ "user",
76
+ "assistant",
77
+ "toolResult",
78
+ "system",
79
+ "custom",
80
+ "custom_message",
81
+ "compaction",
82
+ "branch_summary",
83
+ "model_change",
84
+ "thinking_level_change",
85
+ "context_edit",
86
+ "usage",
87
+ "label",
88
+ "session_info",
89
+ ].map((kind) => (
90
+ <Select.Item key={kind} value={kind}>
91
+ {kind}
92
+ </Select.Item>
93
+ ))}
94
+ </Select.Content>
95
+ </Select.Root>
96
+ </label>
97
+ </details>
98
+ </aside>
99
+ );
100
+ }
@@ -0,0 +1,114 @@
1
+ .observed-context,
2
+ .preview-context,
3
+ .session-primary {
4
+ display: flex;
5
+ flex-direction: column;
6
+ flex: 1;
7
+ min-height: 0;
8
+ min-width: 0;
9
+ }
10
+ .observed-context[hidden],
11
+ .session-primary[hidden],
12
+ .header-actions [hidden] {
13
+ display: none;
14
+ }
15
+ .context-inventory {
16
+ flex-shrink: 0;
17
+ border-top: 1px solid var(--panel-border);
18
+ padding: 6px 10px;
19
+ color: var(--secondary);
20
+ }
21
+ .context-inventory summary,
22
+ .session-filters summary {
23
+ cursor: pointer;
24
+ }
25
+ .inventory-scroll {
26
+ overflow: auto;
27
+ max-height: 38dvh;
28
+ }
29
+ .preview-heading,
30
+ .session-toolbar {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 8px;
34
+ flex-wrap: wrap;
35
+ padding: 6px 0;
36
+ color: var(--secondary);
37
+ font-size: 12px;
38
+ }
39
+ .preview-heading strong {
40
+ overflow-wrap: anywhere;
41
+ }
42
+ .preview-heading button,
43
+ .session-toolbar > button {
44
+ background: var(--panel-raised);
45
+ color: var(--gray-12);
46
+ border: 1px solid var(--panel-border);
47
+ border-radius: 4px;
48
+ padding: 5px;
49
+ }
50
+ .session-toolbar fieldset {
51
+ margin: 0;
52
+ border: 0;
53
+ flex-wrap: wrap;
54
+ }
55
+ .session-toolbar label {
56
+ display: flex;
57
+ gap: 4px;
58
+ align-items: center;
59
+ }
60
+ .session-filters {
61
+ padding: 8px;
62
+ overflow: auto;
63
+ }
64
+ .trace-history .trace-item {
65
+ border-left: 0;
66
+ }
67
+ .trace-history .trace-item::before {
68
+ display: none;
69
+ }
70
+ .turn-heading {
71
+ padding: 10px 8px 5px;
72
+ color: var(--muted);
73
+ font-weight: 600;
74
+ }
75
+ .execution-primary .live-drawer {
76
+ flex: 1;
77
+ max-height: none;
78
+ min-height: 0;
79
+ display: flex;
80
+ flex-direction: column;
81
+ }
82
+ .execution-primary .live-drawer-body {
83
+ flex: 1;
84
+ min-height: 0;
85
+ max-height: none;
86
+ }
87
+ .execution-primary .live-drawer-toggle {
88
+ flex-wrap: wrap;
89
+ flex-shrink: 0;
90
+ }
91
+ .call-summary {
92
+ flex: 2;
93
+ min-width: 0;
94
+ white-space: nowrap;
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ color: var(--muted);
98
+ }
99
+ .live-controls {
100
+ flex-wrap: wrap;
101
+ }
102
+ .recorded-content .code-preview {
103
+ white-space: pre-wrap;
104
+ overflow-wrap: anywhere;
105
+ }
106
+ @media (max-width: 680px) {
107
+ .brand .rt-Heading {
108
+ display: none;
109
+ }
110
+ .session-toolbar {
111
+ font-size: 11px;
112
+ gap: 4px;
113
+ }
114
+ }