paseo-plugin-genui 0.0.0-stage → 0.1.1

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 (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +461 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +182 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,998 @@
1
+ import { Icon, TextInput, copyText as copyToClipboard, useToast } from "@getpaseo/plugin/client/react-native";
2
+ import { ExternalLink } from "@getpaseo/plugin/client/ui";
3
+ import React, { useMemo, useState } from "react";
4
+ import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
5
+ import { formatRatio, readBenchmark } from "../../shared/core/benchmark";
6
+ import {
7
+ coverageTone,
8
+ formatPercent as formatCovPercent,
9
+ headline,
10
+ percent as covPercent,
11
+ rangeLabel,
12
+ readCoverage,
13
+ } from "../../shared/core/coverage";
14
+ import { groupOutdated, readDependencies } from "../../shared/core/dependencies";
15
+ import {
16
+ findingLocation,
17
+ fixAllMessage,
18
+ fixThisMessage,
19
+ readFindings,
20
+ severityLabel,
21
+ } from "../../shared/core/findings";
22
+ import { readLogs, type LogLevel } from "../../shared/core/logs";
23
+ import { formatAmount, formatMs, formatPercent as formatPerfPercent } from "../../shared/core/perf-units";
24
+ import { formatElapsed, jobCountsText, readPipeline } from "../../shared/core/pipeline";
25
+ import { readTrace, sourcePath } from "../../shared/core/stack-trace";
26
+ import { countsText, formatDuration, readTestRun } from "../../shared/core/test-results";
27
+ import { findNode, readTreemap, trail, visibleChildren } from "../../shared/core/treemap";
28
+ import { readWaterfall } from "../../shared/core/waterfall";
29
+ import {
30
+ CHILD_PAGE,
31
+ copyText as formatJsonCopy,
32
+ defaultOpen,
33
+ sizeText,
34
+ visibleRows as visibleJsonRows,
35
+ } from "../../shared/logic/json-logic";
36
+ import { finiteNumber, optionalText, text, type Props } from "../../shared/logic/values";
37
+ import { ErrorLine } from "../node-renderer";
38
+ import { propsKey, toneColor, usePersistedState, useUiRuntime } from "../runtime";
39
+
40
+ export function Logs({ props }: { props: Props }) {
41
+ const { theme } = useUiRuntime();
42
+ const parsed = useMemo(() => readLogs(props.output), [props.output]);
43
+ const command = optionalText(props.command);
44
+ const exit = finiteNumber(props.exit);
45
+ const [query, setQuery] = useState("");
46
+ const [levelFilter, setLevelFilter] = useState<LogLevel | "all">("all");
47
+
48
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
49
+ const { output } = parsed;
50
+
51
+ const filtered = useMemo(() => {
52
+ const q = query.trim().toLowerCase();
53
+ return output.lines.filter((line) => {
54
+ if (levelFilter !== "all" && line.level !== levelFilter) return false;
55
+ if (q && !line.text.toLowerCase().includes(q)) return false;
56
+ return true;
57
+ });
58
+ }, [output.lines, levelFilter, query]);
59
+
60
+ const shown = filtered.slice(-150);
61
+
62
+ return (
63
+ <View
64
+ style={{
65
+ borderRadius: 8,
66
+ borderWidth: StyleSheet.hairlineWidth,
67
+ borderColor: theme.colors.border,
68
+ backgroundColor: theme.colors.surface2,
69
+ overflow: "hidden",
70
+ }}
71
+ >
72
+ <View
73
+ style={{
74
+ flexDirection: "row",
75
+ flexWrap: "wrap",
76
+ justifyContent: "space-between",
77
+ alignItems: "center",
78
+ gap: 8,
79
+ paddingHorizontal: 10,
80
+ paddingVertical: 7,
81
+ borderBottomWidth: StyleSheet.hairlineWidth,
82
+ borderBottomColor: theme.colors.border,
83
+ }}
84
+ >
85
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
86
+ {command ? (
87
+ <Text style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12 }}>
88
+ $ {command}
89
+ </Text>
90
+ ) : (
91
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
92
+ {output.lines.length} lines
93
+ </Text>
94
+ )}
95
+ {exit !== null ? (
96
+ <Text
97
+ style={{
98
+ color: exit === 0 ? theme.colors.statusSuccess : theme.colors.statusDanger,
99
+ fontSize: 11.5,
100
+ fontWeight: "600",
101
+ }}
102
+ >
103
+ exit {exit}
104
+ </Text>
105
+ ) : null}
106
+ </View>
107
+ <View style={{ flexDirection: "row", gap: 6 }}>
108
+ {(["all", "error", "warn", "info"] as const).map((lvl) => (
109
+ <Pressable
110
+ key={lvl}
111
+ onPress={() => setLevelFilter(lvl)}
112
+ style={{
113
+ paddingHorizontal: 6,
114
+ paddingVertical: 2,
115
+ borderRadius: 4,
116
+ backgroundColor: levelFilter === lvl ? theme.colors.accent : theme.colors.surface1,
117
+ }}
118
+ >
119
+ <Text
120
+ style={{
121
+ color: levelFilter === lvl ? theme.colors.accentForeground : theme.colors.foregroundMuted,
122
+ fontSize: 11,
123
+ }}
124
+ >
125
+ {lvl}
126
+ </Text>
127
+ </Pressable>
128
+ ))}
129
+ </View>
130
+ </View>
131
+ <View style={{ paddingHorizontal: 10, paddingVertical: 6, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: theme.colors.border }}>
132
+ <TextInput
133
+ value={query}
134
+ onChangeText={setQuery}
135
+ placeholder="Filter logs…"
136
+ placeholderTextColor={theme.colors.foregroundMuted}
137
+ style={{
138
+ color: theme.colors.foreground,
139
+ fontSize: 12,
140
+ paddingVertical: 2,
141
+ }}
142
+ />
143
+ </View>
144
+ <ScrollView horizontal>
145
+ <View style={{ padding: 10, gap: 2 }}>
146
+ {shown.map((line) => {
147
+ const color =
148
+ line.level === "error" || line.level === "fatal"
149
+ ? theme.colors.statusDanger
150
+ : line.level === "warn"
151
+ ? theme.colors.statusWarning
152
+ : theme.colors.foreground;
153
+ return (
154
+ <View key={line.number} style={{ flexDirection: "row", gap: 8 }}>
155
+ <Text
156
+ style={{
157
+ width: 34,
158
+ textAlign: "right",
159
+ color: theme.colors.foregroundMuted,
160
+ fontFamily: "monospace",
161
+ fontSize: 11,
162
+ }}
163
+ >
164
+ {line.number}
165
+ </Text>
166
+ <Text selectable style={{ color, fontFamily: "monospace", fontSize: 11.5 }}>
167
+ {line.text}
168
+ </Text>
169
+ </View>
170
+ );
171
+ })}
172
+ </View>
173
+ </ScrollView>
174
+ </View>
175
+ );
176
+ }
177
+
178
+ export function StackTrace({ props }: { props: Props }) {
179
+ const { theme, openFile } = useUiRuntime();
180
+ const parsed = useMemo(() => readTrace(props.trace), [props.trace]);
181
+ const title = optionalText(props.title);
182
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
183
+ const { trace } = parsed;
184
+
185
+ return (
186
+ <View
187
+ style={{
188
+ borderRadius: 8,
189
+ borderWidth: StyleSheet.hairlineWidth,
190
+ borderColor: theme.colors.statusDanger,
191
+ backgroundColor: theme.colors.surface2,
192
+ padding: 12,
193
+ gap: 10,
194
+ }}
195
+ >
196
+ {title ? (
197
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>{title}</Text>
198
+ ) : null}
199
+ {trace.sections.map((sec, sIdx) => (
200
+ <View key={sIdx} style={{ gap: 6 }}>
201
+ <Text selectable style={{ color: theme.colors.statusDanger, fontSize: 13, fontWeight: "700", fontFamily: "monospace" }}>
202
+ {sec.type ? `${sec.type}: ${sec.message}` : sec.message}
203
+ </Text>
204
+ <View style={{ gap: 4 }}>
205
+ {sec.frames.slice(0, 12).map((frame, fIdx) => {
206
+ const path = sourcePath(frame);
207
+ return (
208
+ <Pressable
209
+ key={fIdx}
210
+ onPress={() => {
211
+ if (path && openFile) void openFile(path, frame.location?.line ?? undefined);
212
+ }}
213
+ style={{
214
+ paddingHorizontal: 8,
215
+ paddingVertical: 4,
216
+ borderRadius: 4,
217
+ backgroundColor: frame.app ? theme.colors.surface1 : "transparent",
218
+ }}
219
+ >
220
+ <Text
221
+ selectable
222
+ style={{
223
+ color: frame.app ? theme.colors.foreground : theme.colors.foregroundMuted,
224
+ fontFamily: "monospace",
225
+ fontSize: 11.5,
226
+ }}
227
+ >
228
+ {frame.text}
229
+ </Text>
230
+ </Pressable>
231
+ );
232
+ })}
233
+ </View>
234
+ </View>
235
+ ))}
236
+ </View>
237
+ );
238
+ }
239
+
240
+ export function Findings({ props }: { props: Props }) {
241
+ const { theme, run, openFile } = useUiRuntime();
242
+ const parsed = useMemo(() => readFindings(props.items), [props.items]);
243
+ const title = optionalText(props.title);
244
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
245
+ const { report } = parsed;
246
+
247
+ return (
248
+ <View style={{ gap: 8 }}>
249
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
250
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>
251
+ {title ?? `${report.findings.length} finding${report.findings.length === 1 ? "" : "s"}`}
252
+ </Text>
253
+ {report.findings.length > 1 ? (
254
+ <Pressable
255
+ onPress={() => {
256
+ const msg = fixAllMessage(report.findings, report.scale);
257
+ if (msg) void run([{ kind: "action", type: "send", text: msg }]);
258
+ }}
259
+ style={{
260
+ paddingHorizontal: 10,
261
+ paddingVertical: 4,
262
+ borderRadius: 6,
263
+ backgroundColor: theme.colors.accent,
264
+ }}
265
+ >
266
+ <Text style={{ color: theme.colors.accentForeground, fontSize: 11.5, fontWeight: "600" }}>
267
+ Fix all
268
+ </Text>
269
+ </Pressable>
270
+ ) : null}
271
+ </View>
272
+ {report.findings.map((item, idx) => {
273
+ const loc = findingLocation(item);
274
+ const sevColor =
275
+ item.severity === "critical" || item.severity === "high"
276
+ ? theme.colors.statusDanger
277
+ : item.severity === "medium"
278
+ ? theme.colors.statusWarning
279
+ : theme.colors.accent;
280
+ return (
281
+ <View
282
+ key={idx}
283
+ style={{
284
+ borderRadius: 8,
285
+ borderWidth: StyleSheet.hairlineWidth,
286
+ borderColor: theme.colors.border,
287
+ backgroundColor: theme.colors.surface2,
288
+ padding: 10,
289
+ gap: 6,
290
+ }}
291
+ >
292
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center", gap: 8 }}>
293
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600", flex: 1 }}>
294
+ {item.title}
295
+ </Text>
296
+ <Text style={{ color: sevColor, fontSize: 11, fontWeight: "700", textTransform: "uppercase" }}>
297
+ {severityLabel(item.severity, report.scale)}
298
+ </Text>
299
+ </View>
300
+ {loc ? (
301
+ <Pressable
302
+ onPress={() => {
303
+ if (item.file && openFile) void openFile(item.file, item.line ?? undefined);
304
+ }}
305
+ >
306
+ <Text style={{ color: theme.colors.accent, fontFamily: "monospace", fontSize: 11.5 }}>
307
+ {loc}
308
+ </Text>
309
+ </Pressable>
310
+ ) : null}
311
+ {item.detail ? (
312
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{item.detail}</Text>
313
+ ) : null}
314
+ <Pressable
315
+ onPress={() => {
316
+ const msg = fixThisMessage(item);
317
+ void run([{ kind: "action", type: "send", text: msg }]);
318
+ }}
319
+ style={{
320
+ alignSelf: "flex-start",
321
+ paddingHorizontal: 8,
322
+ paddingVertical: 4,
323
+ borderRadius: 6,
324
+ backgroundColor: theme.colors.surface1,
325
+ }}
326
+ >
327
+ <Text style={{ color: theme.colors.foreground, fontSize: 11.5, fontWeight: "500" }}>
328
+ Fix this
329
+ </Text>
330
+ </Pressable>
331
+ </View>
332
+ );
333
+ })}
334
+ </View>
335
+ );
336
+ }
337
+
338
+ export function Coverage({ props }: { props: Props }) {
339
+ const { theme } = useUiRuntime();
340
+ const parsed = useMemo(() => readCoverage(props.report), [props.report]);
341
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
342
+ const { report } = parsed;
343
+ const totalRatio = headline(report.total, report.unit);
344
+ const totalPct = covPercent(totalRatio);
345
+
346
+ return (
347
+ <View style={{ gap: 8 }}>
348
+ {totalPct !== null ? (
349
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
350
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>
351
+ Total {report.unit} coverage
352
+ </Text>
353
+ <Text
354
+ style={{
355
+ color: toneColor(theme, coverageTone(totalPct)),
356
+ fontSize: 15,
357
+ fontWeight: "700",
358
+ }}
359
+ >
360
+ {formatCovPercent(totalPct)} ({totalRatio?.covered}/{totalRatio?.total})
361
+ </Text>
362
+ </View>
363
+ ) : null}
364
+ <View
365
+ style={{
366
+ borderRadius: 8,
367
+ borderWidth: StyleSheet.hairlineWidth,
368
+ borderColor: theme.colors.border,
369
+ overflow: "hidden",
370
+ }}
371
+ >
372
+ {report.files.slice(0, 25).map((file, idx) => {
373
+ const ratio = headline(file, report.unit);
374
+ const pct = covPercent(ratio) ?? 0;
375
+ return (
376
+ <View
377
+ key={file.path}
378
+ style={{
379
+ paddingHorizontal: 10,
380
+ paddingVertical: 8,
381
+ borderTopWidth: idx === 0 ? 0 : StyleSheet.hairlineWidth,
382
+ borderTopColor: theme.colors.border,
383
+ gap: 3,
384
+ }}
385
+ >
386
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
387
+ <Text style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12, flex: 1 }}>
388
+ {file.path}
389
+ </Text>
390
+ <Text style={{ color: toneColor(theme, coverageTone(pct)), fontSize: 12, fontWeight: "600" }}>
391
+ {formatCovPercent(pct)}
392
+ </Text>
393
+ </View>
394
+ {file.uncovered && file.uncovered.length > 0 ? (
395
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11, fontFamily: "monospace" }}>
396
+ Uncovered lines: {file.uncovered.slice(0, 6).map(rangeLabel).join(", ")}
397
+ </Text>
398
+ ) : null}
399
+ </View>
400
+ );
401
+ })}
402
+ </View>
403
+ </View>
404
+ );
405
+ }
406
+
407
+ export function Dependencies({ props }: { props: Props }) {
408
+ const { theme } = useUiRuntime();
409
+ const parsed = useMemo(() => readDependencies(props.report), [props.report]);
410
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
411
+ const { report } = parsed;
412
+ const groups = groupOutdated(report.outdated);
413
+
414
+ return (
415
+ <View style={{ gap: 10 }}>
416
+ {report.vulnerable.length > 0 ? (
417
+ <View style={{ gap: 6 }}>
418
+ <Text style={{ color: theme.colors.statusDanger, fontSize: 12.5, fontWeight: "600" }}>
419
+ Vulnerabilities ({report.vulnerable.length})
420
+ </Text>
421
+ {report.vulnerable.map((v, idx) => (
422
+ <View
423
+ key={idx}
424
+ style={{
425
+ padding: 8,
426
+ borderRadius: 6,
427
+ backgroundColor: theme.colors.surface2,
428
+ borderWidth: StyleSheet.hairlineWidth,
429
+ borderColor: theme.colors.statusDanger,
430
+ gap: 2,
431
+ }}
432
+ >
433
+ <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
434
+ <Text style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12, fontWeight: "600" }}>
435
+ {v.name}
436
+ </Text>
437
+ <Text style={{ color: theme.colors.statusDanger, fontSize: 11, fontWeight: "700" }}>
438
+ {v.severity}
439
+ </Text>
440
+ </View>
441
+ {v.advisories[0]?.title ? (
442
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
443
+ {v.advisories[0].title}
444
+ </Text>
445
+ ) : null}
446
+ </View>
447
+ ))}
448
+ </View>
449
+ ) : null}
450
+ {groups.map((group) => (
451
+ <View key={group.key} style={{ gap: 4 }}>
452
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, fontWeight: "600", textTransform: "uppercase" }}>
453
+ {group.key} updates ({group.packages.length})
454
+ </Text>
455
+ <View
456
+ style={{
457
+ borderRadius: 8,
458
+ borderWidth: StyleSheet.hairlineWidth,
459
+ borderColor: theme.colors.border,
460
+ overflow: "hidden",
461
+ }}
462
+ >
463
+ {group.packages.map((pkg, idx) => (
464
+ <View
465
+ key={pkg.name}
466
+ style={{
467
+ flexDirection: "row",
468
+ justifyContent: "space-between",
469
+ paddingHorizontal: 10,
470
+ paddingVertical: 7,
471
+ borderTopWidth: idx === 0 ? 0 : StyleSheet.hairlineWidth,
472
+ borderTopColor: theme.colors.border,
473
+ }}
474
+ >
475
+ <Text style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12 }}>
476
+ {pkg.name}
477
+ </Text>
478
+ <Text style={{ color: theme.colors.foregroundMuted, fontFamily: "monospace", fontSize: 12 }}>
479
+ {pkg.current} → <Text style={{ color: theme.colors.accent }}>{pkg.latest}</Text>
480
+ </Text>
481
+ </View>
482
+ ))}
483
+ </View>
484
+ </View>
485
+ ))}
486
+ </View>
487
+ );
488
+ }
489
+
490
+ export function Pipeline({ props }: { props: Props }) {
491
+ const { theme } = useUiRuntime();
492
+ const parsed = useMemo(() => readPipeline(props.run), [props.run]);
493
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
494
+ const { run } = parsed;
495
+
496
+ return (
497
+ <View
498
+ style={{
499
+ borderRadius: 8,
500
+ borderWidth: StyleSheet.hairlineWidth,
501
+ borderColor: theme.colors.border,
502
+ backgroundColor: theme.colors.surface2,
503
+ padding: 10,
504
+ gap: 8,
505
+ }}
506
+ >
507
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
508
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>
509
+ {run.title ?? run.name ?? "Pipeline"}
510
+ </Text>
511
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
512
+ {jobCountsText(run.counts)}
513
+ </Text>
514
+ </View>
515
+ {run.stages.map((stage, sIdx) => (
516
+ <View key={sIdx} style={{ gap: 4 }}>
517
+ {stage.name ? (
518
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, fontWeight: "600" }}>
519
+ {stage.name}
520
+ </Text>
521
+ ) : null}
522
+ {stage.jobs.map((job, jIdx) => {
523
+ const color =
524
+ job.status === "success"
525
+ ? theme.colors.statusSuccess
526
+ : job.status === "failure"
527
+ ? theme.colors.statusDanger
528
+ : job.status === "in_progress"
529
+ ? theme.colors.accent
530
+ : theme.colors.foregroundMuted;
531
+ return (
532
+ <View
533
+ key={jIdx}
534
+ style={{
535
+ flexDirection: "row",
536
+ justifyContent: "space-between",
537
+ alignItems: "center",
538
+ paddingHorizontal: 8,
539
+ paddingVertical: 6,
540
+ borderRadius: 6,
541
+ backgroundColor: theme.colors.surface1,
542
+ }}
543
+ >
544
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, flex: 1 }}>
545
+ <View style={{ width: 7, height: 7, borderRadius: 4, backgroundColor: color }} />
546
+ {job.url ? (
547
+ <ExternalLink href={job.url}>
548
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{job.name}</Text>
549
+ </ExternalLink>
550
+ ) : (
551
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{job.name}</Text>
552
+ )}
553
+ </View>
554
+ {job.ms !== null ? (
555
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
556
+ {formatElapsed(job.ms)}
557
+ </Text>
558
+ ) : null}
559
+ </View>
560
+ );
561
+ })}
562
+ </View>
563
+ ))}
564
+ </View>
565
+ );
566
+ }
567
+
568
+ export function Benchmark({ props }: { props: Props }) {
569
+ const { theme } = useUiRuntime();
570
+ const unit = optionalText(props.unit);
571
+ const parsed = useMemo(() => readBenchmark(props.results, unit), [props.results, unit]);
572
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
573
+ const { report } = parsed;
574
+
575
+ if (report.kind === "compare") {
576
+ return (
577
+ <View
578
+ style={{
579
+ borderRadius: 8,
580
+ borderWidth: StyleSheet.hairlineWidth,
581
+ borderColor: theme.colors.border,
582
+ overflow: "hidden",
583
+ }}
584
+ >
585
+ {report.rows.map((row, idx) => {
586
+ const color =
587
+ row.verdict === "better"
588
+ ? theme.colors.statusSuccess
589
+ : row.verdict === "worse"
590
+ ? theme.colors.statusDanger
591
+ : theme.colors.foregroundMuted;
592
+ return (
593
+ <View
594
+ key={idx}
595
+ style={{
596
+ flexDirection: "row",
597
+ justifyContent: "space-between",
598
+ alignItems: "center",
599
+ paddingHorizontal: 10,
600
+ paddingVertical: 8,
601
+ borderTopWidth: idx === 0 ? 0 : StyleSheet.hairlineWidth,
602
+ borderTopColor: theme.colors.border,
603
+ }}
604
+ >
605
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, flex: 1 }}>{row.name}</Text>
606
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, fontFamily: "monospace" }}>
607
+ {formatAmount(row.before, row.unit)} → {formatAmount(row.after, row.unit)}{" "}
608
+ {row.percent !== null ? (
609
+ <Text style={{ color, fontWeight: "600" }}>({formatPerfPercent(row.percent)})</Text>
610
+ ) : null}
611
+ </Text>
612
+ </View>
613
+ );
614
+ })}
615
+ </View>
616
+ );
617
+ }
618
+
619
+ if (report.kind === "runs") {
620
+ return (
621
+ <View style={{ gap: 8 }}>
622
+ {report.groups.map((group, gIdx) => (
623
+ <View
624
+ key={gIdx}
625
+ style={{
626
+ borderRadius: 8,
627
+ borderWidth: StyleSheet.hairlineWidth,
628
+ borderColor: theme.colors.border,
629
+ overflow: "hidden",
630
+ }}
631
+ >
632
+ {group.runs.map((run, rIdx) => (
633
+ <View
634
+ key={rIdx}
635
+ style={{
636
+ flexDirection: "row",
637
+ justifyContent: "space-between",
638
+ paddingHorizontal: 10,
639
+ paddingVertical: 8,
640
+ borderTopWidth: rIdx === 0 ? 0 : StyleSheet.hairlineWidth,
641
+ borderTopColor: theme.colors.border,
642
+ }}
643
+ >
644
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, flex: 1 }}>{run.name}</Text>
645
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, fontFamily: "monospace" }}>
646
+ {formatAmount(run.mean, report.unit)}{" "}
647
+ {run.ratio > 1 ? `(${formatRatio(run.ratio, run.ratioStddev)}×)` : "(fastest)"}
648
+ </Text>
649
+ </View>
650
+ ))}
651
+ </View>
652
+ ))}
653
+ </View>
654
+ );
655
+ }
656
+
657
+ return (
658
+ <View
659
+ style={{
660
+ borderRadius: 8,
661
+ borderWidth: StyleSheet.hairlineWidth,
662
+ borderColor: theme.colors.border,
663
+ overflow: "hidden",
664
+ }}
665
+ >
666
+ {report.runs.map((run, idx) => (
667
+ <View
668
+ key={idx}
669
+ style={{
670
+ flexDirection: "row",
671
+ justifyContent: "space-between",
672
+ paddingHorizontal: 10,
673
+ paddingVertical: 8,
674
+ borderTopWidth: idx === 0 ? 0 : StyleSheet.hairlineWidth,
675
+ borderTopColor: theme.colors.border,
676
+ }}
677
+ >
678
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{run.name}</Text>
679
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, fontFamily: "monospace" }}>
680
+ {Object.entries(run.values)
681
+ .map(([k, v]) => `${k}: ${v}`)
682
+ .join(" · ")}
683
+ </Text>
684
+ </View>
685
+ ))}
686
+ </View>
687
+ );
688
+ }
689
+
690
+ export function Waterfall({ props }: { props: Props }) {
691
+ const { theme } = useUiRuntime();
692
+ const unit = optionalText(props.unit);
693
+ const parsed = useMemo(() => readWaterfall(props.spans, unit), [props.spans, unit]);
694
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
695
+ const { waterfall } = parsed;
696
+ const total = Math.max(1, waterfall.total);
697
+
698
+ return (
699
+ <View style={{ gap: 6 }}>
700
+ {waterfall.spans.slice(0, 40).map((span) => {
701
+ const leftPct = Math.min(95, Math.max(0, (span.start / total) * 100));
702
+ const widthPct = Math.min(100 - leftPct, Math.max(2, ((span.end - span.start) / total) * 100));
703
+ return (
704
+ <View key={span.index} style={{ gap: 2 }}>
705
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
706
+ <Text
707
+ numberOfLines={1}
708
+ style={{
709
+ color: theme.colors.foreground,
710
+ fontSize: 12,
711
+ paddingLeft: span.depth * 10,
712
+ flex: 1,
713
+ }}
714
+ >
715
+ {span.name}
716
+ </Text>
717
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5, fontFamily: "monospace" }}>
718
+ {formatMs(span.end - span.start)}
719
+ </Text>
720
+ </View>
721
+ <View style={{ height: 7, borderRadius: 3.5, backgroundColor: theme.colors.surface2 }}>
722
+ <View
723
+ style={{
724
+ marginLeft: `${leftPct}%`,
725
+ width: `${widthPct}%`,
726
+ height: "100%",
727
+ borderRadius: 3.5,
728
+ backgroundColor: theme.colors.accent,
729
+ }}
730
+ />
731
+ </View>
732
+ </View>
733
+ );
734
+ })}
735
+ </View>
736
+ );
737
+ }
738
+
739
+ export function Treemap({ props }: { props: Props }) {
740
+ const { theme } = useUiRuntime();
741
+ const unit = optionalText(props.unit);
742
+ const parsed = useMemo(() => readTreemap(props.items, unit), [props.items, unit]);
743
+ const [focusPath, setFocusPath] = useState("");
744
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
745
+ const { treemap } = parsed;
746
+ const current = findNode(treemap.root, focusPath) ?? treemap.root;
747
+ const crumbs = trail(treemap.root, current.path);
748
+ const { nodes } = visibleChildren(current, 16);
749
+ const total = Math.max(1, current.value);
750
+
751
+ return (
752
+ <View style={{ gap: 8 }}>
753
+ {crumbs.length > 1 ? (
754
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 4 }}>
755
+ {crumbs.map((c, idx) => (
756
+ <Pressable key={c.path} onPress={() => setFocusPath(c.path)}>
757
+ <Text style={{ color: theme.colors.accent, fontSize: 12 }}>
758
+ {c.name || "root"}
759
+ {idx < crumbs.length - 1 ? " / " : ""}
760
+ </Text>
761
+ </Pressable>
762
+ ))}
763
+ </View>
764
+ ) : null}
765
+ <View style={{ gap: 6 }}>
766
+ {nodes.map((node) => {
767
+ const pct = Math.round((node.value / total) * 100);
768
+ return (
769
+ <Pressable
770
+ key={node.path}
771
+ onPress={() => {
772
+ if (node.children.length > 0) setFocusPath(node.path);
773
+ }}
774
+ style={{
775
+ padding: 8,
776
+ borderRadius: 6,
777
+ backgroundColor: theme.colors.surface2,
778
+ borderWidth: StyleSheet.hairlineWidth,
779
+ borderColor: theme.colors.border,
780
+ gap: 4,
781
+ }}
782
+ >
783
+ <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
784
+ <Text style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12 }}>
785
+ {node.name}
786
+ </Text>
787
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
788
+ {formatAmount(node.value, treemap.unit)} ({pct}%)
789
+ </Text>
790
+ </View>
791
+ <View style={{ height: 6, borderRadius: 3, backgroundColor: theme.colors.surface1 }}>
792
+ <View
793
+ style={{
794
+ width: `${Math.max(2, pct)}%`,
795
+ height: "100%",
796
+ borderRadius: 3,
797
+ backgroundColor: theme.colors.accent,
798
+ }}
799
+ />
800
+ </View>
801
+ </Pressable>
802
+ );
803
+ })}
804
+ </View>
805
+ </View>
806
+ );
807
+ }
808
+
809
+ export function Tests({ props }: { props: Props }) {
810
+ const { theme } = useUiRuntime();
811
+ const parsed = useMemo(() => readTestRun(props.results), [props.results]);
812
+ const title = optionalText(props.title);
813
+ if (!parsed.ok) return <ErrorLine message={parsed.error} />;
814
+ const { run } = parsed;
815
+
816
+ return (
817
+ <View
818
+ style={{
819
+ borderRadius: 8,
820
+ borderWidth: StyleSheet.hairlineWidth,
821
+ borderColor: theme.colors.border,
822
+ backgroundColor: theme.colors.surface2,
823
+ padding: 10,
824
+ gap: 8,
825
+ }}
826
+ >
827
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
828
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>
829
+ {title ?? "Test results"}
830
+ </Text>
831
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
832
+ {countsText(run.counts)}
833
+ {run.ms !== null ? ` · ${formatDuration(run.ms)}` : ""}
834
+ </Text>
835
+ </View>
836
+ <View style={{ gap: 4 }}>
837
+ {run.tests.slice(0, 30).map((tc, idx) => {
838
+ const color =
839
+ tc.status === "passed"
840
+ ? theme.colors.statusSuccess
841
+ : tc.status === "failed"
842
+ ? theme.colors.statusDanger
843
+ : theme.colors.foregroundMuted;
844
+ return (
845
+ <View
846
+ key={idx}
847
+ style={{
848
+ padding: 8,
849
+ borderRadius: 6,
850
+ backgroundColor: theme.colors.surface1,
851
+ gap: 4,
852
+ }}
853
+ >
854
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
855
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, flex: 1 }}>
856
+ <Text style={{ color, fontWeight: "700" }}>
857
+ {tc.status === "passed" ? "✓ " : tc.status === "failed" ? "✗ " : "○ "}
858
+ </Text>
859
+ {tc.name}
860
+ </Text>
861
+ {tc.ms !== null ? (
862
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
863
+ {formatDuration(tc.ms)}
864
+ </Text>
865
+ ) : null}
866
+ </View>
867
+ {tc.message ? (
868
+ <Text
869
+ selectable
870
+ style={{ color: theme.colors.statusDanger, fontFamily: "monospace", fontSize: 11.5 }}
871
+ >
872
+ {tc.message}
873
+ </Text>
874
+ ) : null}
875
+ </View>
876
+ );
877
+ })}
878
+ </View>
879
+ </View>
880
+ );
881
+ }
882
+
883
+ export function Json({ props }: { props: Props }) {
884
+ const { theme } = useUiRuntime();
885
+ const toast = useToast();
886
+ const root = props.value ?? null;
887
+ const label = optionalText(props.label);
888
+ const initial = useMemo(() => [...defaultOpen(root)], [root]);
889
+ const [openIds, setOpenIds] = usePersistedState<string[]>(
890
+ `json:${propsKey(root)}`,
891
+ initial,
892
+ (v) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : null),
893
+ );
894
+ const openSet = useMemo(() => new Set(["$", ...openIds]), [openIds]);
895
+ const rows = useMemo(
896
+ () => visibleJsonRows(root, (id) => openSet.has(id), () => CHILD_PAGE),
897
+ [root, openSet],
898
+ );
899
+
900
+ return (
901
+ <View
902
+ style={{
903
+ borderRadius: 8,
904
+ borderWidth: StyleSheet.hairlineWidth,
905
+ borderColor: theme.colors.border,
906
+ backgroundColor: theme.colors.surface2,
907
+ overflow: "hidden",
908
+ }}
909
+ >
910
+ <View
911
+ style={{
912
+ flexDirection: "row",
913
+ justifyContent: "space-between",
914
+ alignItems: "center",
915
+ paddingHorizontal: 10,
916
+ paddingVertical: 6,
917
+ borderBottomWidth: StyleSheet.hairlineWidth,
918
+ borderBottomColor: theme.colors.border,
919
+ }}
920
+ >
921
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
922
+ {label ?? "JSON"} · {sizeText(root)}
923
+ </Text>
924
+ <Pressable
925
+ onPress={() => {
926
+ void copyToClipboard(formatJsonCopy(root)).then(
927
+ () => toast.show("Copied JSON", { variant: "success" }),
928
+ () => toast.error("Could not copy"),
929
+ );
930
+ }}
931
+ >
932
+ <Icon name="Copy" size={13} color={theme.colors.foregroundMuted} />
933
+ </Pressable>
934
+ </View>
935
+ <View style={{ paddingVertical: 6 }}>
936
+ {rows.slice(0, 80).map((row) => {
937
+ if (row.type === "more") {
938
+ return (
939
+ <Text
940
+ key={row.id}
941
+ style={{
942
+ paddingLeft: 10 + row.depth * 14,
943
+ color: theme.colors.foregroundMuted,
944
+ fontSize: 11.5,
945
+ }}
946
+ >
947
+ … {row.remaining} more
948
+ </Text>
949
+ );
950
+ }
951
+ const expandable = row.children > 0;
952
+ const isOpen = openSet.has(row.id);
953
+ return (
954
+ <Pressable
955
+ key={row.id}
956
+ onPress={() => {
957
+ if (!expandable) return;
958
+ setOpenIds((prev) =>
959
+ prev.includes(row.id) ? prev.filter((x) => x !== row.id) : [...prev, row.id],
960
+ );
961
+ }}
962
+ style={{
963
+ flexDirection: "row",
964
+ alignItems: "center",
965
+ paddingLeft: 10 + row.depth * 14,
966
+ paddingRight: 10,
967
+ paddingVertical: 3,
968
+ gap: 6,
969
+ }}
970
+ >
971
+ {expandable ? (
972
+ <Icon
973
+ name={isOpen ? "ChevronDown" : "ChevronRight"}
974
+ size={12}
975
+ color={theme.colors.foregroundMuted}
976
+ />
977
+ ) : (
978
+ <View style={{ width: 12 }} />
979
+ )}
980
+ {row.key !== null ? (
981
+ <Text style={{ color: theme.colors.accent, fontFamily: "monospace", fontSize: 12 }}>
982
+ {String(row.key)}:
983
+ </Text>
984
+ ) : null}
985
+ <Text
986
+ selectable
987
+ numberOfLines={1}
988
+ style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12, flex: 1 }}
989
+ >
990
+ {expandable ? sizeText(row.value) : JSON.stringify(row.value)}
991
+ </Text>
992
+ </Pressable>
993
+ );
994
+ })}
995
+ </View>
996
+ </View>
997
+ );
998
+ }