@flanksource/clicky-ui 0.3.47 → 0.3.49

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 (255) hide show
  1. package/README.md +49 -0
  2. package/dist/comments/CommentSidePanel.cjs +10 -4
  3. package/dist/comments/CommentSidePanel.cjs.map +1 -1
  4. package/dist/comments/CommentSidePanel.d.ts +2 -0
  5. package/dist/comments/CommentSidePanel.d.ts.map +1 -1
  6. package/dist/comments/CommentSidePanel.js +10 -4
  7. package/dist/comments/CommentSidePanel.js.map +1 -1
  8. package/dist/comments/CommentThread.cjs +2 -0
  9. package/dist/comments/CommentThread.cjs.map +1 -1
  10. package/dist/comments/CommentThread.d.ts +2 -1
  11. package/dist/comments/CommentThread.d.ts.map +1 -1
  12. package/dist/comments/CommentThread.js +2 -0
  13. package/dist/comments/CommentThread.js.map +1 -1
  14. package/dist/comments/CommentThreadList.cjs +3 -0
  15. package/dist/comments/CommentThreadList.cjs.map +1 -1
  16. package/dist/comments/CommentThreadList.d.ts +3 -1
  17. package/dist/comments/CommentThreadList.d.ts.map +1 -1
  18. package/dist/comments/CommentThreadList.js +3 -0
  19. package/dist/comments/CommentThreadList.js.map +1 -1
  20. package/dist/data/CodeLine.cjs +4 -2
  21. package/dist/data/CodeLine.cjs.map +1 -1
  22. package/dist/data/CodeLine.d.ts.map +1 -1
  23. package/dist/data/CodeLine.js +4 -2
  24. package/dist/data/CodeLine.js.map +1 -1
  25. package/dist/data/DataTable.cjs +18 -8
  26. package/dist/data/DataTable.cjs.map +1 -1
  27. package/dist/data/DataTable.d.ts +4 -0
  28. package/dist/data/DataTable.d.ts.map +1 -1
  29. package/dist/data/DataTable.js +18 -8
  30. package/dist/data/DataTable.js.map +1 -1
  31. package/dist/data/SymbolBadge.cjs +61 -0
  32. package/dist/data/SymbolBadge.cjs.map +1 -0
  33. package/dist/data/SymbolBadge.d.ts +21 -0
  34. package/dist/data/SymbolBadge.d.ts.map +1 -0
  35. package/dist/data/SymbolBadge.js +61 -0
  36. package/dist/data/SymbolBadge.js.map +1 -0
  37. package/dist/data/TaskManager.cjs +183 -159
  38. package/dist/data/TaskManager.cjs.map +1 -1
  39. package/dist/data/TaskManager.d.ts +3 -1
  40. package/dist/data/TaskManager.d.ts.map +1 -1
  41. package/dist/data/TaskManager.js +185 -161
  42. package/dist/data/TaskManager.js.map +1 -1
  43. package/dist/data/TaskManagerButton.cjs +1 -0
  44. package/dist/data/TaskManagerButton.cjs.map +1 -1
  45. package/dist/data/TaskManagerButton.js +1 -0
  46. package/dist/data/TaskManagerButton.js.map +1 -1
  47. package/dist/data/TaskProgress.cjs +13 -7
  48. package/dist/data/TaskProgress.cjs.map +1 -1
  49. package/dist/data/TaskProgress.d.ts +3 -1
  50. package/dist/data/TaskProgress.d.ts.map +1 -1
  51. package/dist/data/TaskProgress.js +13 -7
  52. package/dist/data/TaskProgress.js.map +1 -1
  53. package/dist/data/test-runner/TestDetailPanel.cjs +26 -12
  54. package/dist/data/test-runner/TestDetailPanel.cjs.map +1 -1
  55. package/dist/data/test-runner/TestDetailPanel.d.ts.map +1 -1
  56. package/dist/data/test-runner/TestDetailPanel.js +27 -13
  57. package/dist/data/test-runner/TestDetailPanel.js.map +1 -1
  58. package/dist/data/test-runner/TestRunner.cjs +6 -0
  59. package/dist/data/test-runner/TestRunner.cjs.map +1 -1
  60. package/dist/data/test-runner/TestRunner.d.ts +3 -1
  61. package/dist/data/test-runner/TestRunner.d.ts.map +1 -1
  62. package/dist/data/test-runner/TestRunner.js +6 -0
  63. package/dist/data/test-runner/TestRunner.js.map +1 -1
  64. package/dist/data/test-runner/TestTreeNode.cjs +2 -1
  65. package/dist/data/test-runner/TestTreeNode.cjs.map +1 -1
  66. package/dist/data/test-runner/TestTreeNode.d.ts.map +1 -1
  67. package/dist/data/test-runner/TestTreeNode.js +2 -1
  68. package/dist/data/test-runner/TestTreeNode.js.map +1 -1
  69. package/dist/data/test-runner/adapter.cjs +4 -0
  70. package/dist/data/test-runner/adapter.cjs.map +1 -1
  71. package/dist/data/test-runner/adapter.d.ts +9 -0
  72. package/dist/data/test-runner/adapter.d.ts.map +1 -1
  73. package/dist/data/test-runner/adapter.js +4 -0
  74. package/dist/data/test-runner/adapter.js.map +1 -1
  75. package/dist/data/test-runner/context.cjs.map +1 -1
  76. package/dist/data/test-runner/context.d.ts +2 -0
  77. package/dist/data/test-runner/context.d.ts.map +1 -1
  78. package/dist/data/test-runner/context.js.map +1 -1
  79. package/dist/data/test-runner/index.d.ts +2 -2
  80. package/dist/data/test-runner/index.d.ts.map +1 -1
  81. package/dist/data/test-runner/marks.cjs +42 -1
  82. package/dist/data/test-runner/marks.cjs.map +1 -1
  83. package/dist/data/test-runner/marks.d.ts +10 -0
  84. package/dist/data/test-runner/marks.d.ts.map +1 -1
  85. package/dist/data/test-runner/marks.js +42 -1
  86. package/dist/data/test-runner/marks.js.map +1 -1
  87. package/dist/data/traces/ProcessTraceTree.cjs +93 -0
  88. package/dist/data/traces/ProcessTraceTree.cjs.map +1 -0
  89. package/dist/data/traces/ProcessTraceTree.d.ts +24 -0
  90. package/dist/data/traces/ProcessTraceTree.d.ts.map +1 -0
  91. package/dist/data/traces/ProcessTraceTree.js +93 -0
  92. package/dist/data/traces/ProcessTraceTree.js.map +1 -0
  93. package/dist/data/traces/Sparkline.cjs +23 -0
  94. package/dist/data/traces/Sparkline.cjs.map +1 -0
  95. package/dist/data/traces/Sparkline.d.ts +13 -0
  96. package/dist/data/traces/Sparkline.d.ts.map +1 -0
  97. package/dist/data/traces/Sparkline.js +23 -0
  98. package/dist/data/traces/Sparkline.js.map +1 -0
  99. package/dist/data/traces/SqlTraceTable.cjs +104 -0
  100. package/dist/data/traces/SqlTraceTable.cjs.map +1 -0
  101. package/dist/data/traces/SqlTraceTable.d.ts +19 -0
  102. package/dist/data/traces/SqlTraceTable.d.ts.map +1 -0
  103. package/dist/data/traces/SqlTraceTable.js +104 -0
  104. package/dist/data/traces/SqlTraceTable.js.map +1 -0
  105. package/dist/data/traces/TerminalCastPlayer.cjs +162 -0
  106. package/dist/data/traces/TerminalCastPlayer.cjs.map +1 -0
  107. package/dist/data/traces/TerminalCastPlayer.d.ts +21 -0
  108. package/dist/data/traces/TerminalCastPlayer.d.ts.map +1 -0
  109. package/dist/data/traces/TerminalCastPlayer.js +162 -0
  110. package/dist/data/traces/TerminalCastPlayer.js.map +1 -0
  111. package/dist/data/traces/TraceHarView.cjs +15 -0
  112. package/dist/data/traces/TraceHarView.cjs.map +1 -0
  113. package/dist/data/traces/TraceHarView.d.ts +13 -0
  114. package/dist/data/traces/TraceHarView.d.ts.map +1 -0
  115. package/dist/data/traces/TraceHarView.js +15 -0
  116. package/dist/data/traces/TraceHarView.js.map +1 -0
  117. package/dist/data/traces/TraceView.cjs +54 -0
  118. package/dist/data/traces/TraceView.cjs.map +1 -0
  119. package/dist/data/traces/TraceView.d.ts +18 -0
  120. package/dist/data/traces/TraceView.d.ts.map +1 -0
  121. package/dist/data/traces/TraceView.js +54 -0
  122. package/dist/data/traces/TraceView.js.map +1 -0
  123. package/dist/data/traces/cast.cjs +81 -0
  124. package/dist/data/traces/cast.cjs.map +1 -0
  125. package/dist/data/traces/cast.d.ts +9 -0
  126. package/dist/data/traces/cast.d.ts.map +1 -0
  127. package/dist/data/traces/cast.js +81 -0
  128. package/dist/data/traces/cast.js.map +1 -0
  129. package/dist/data/traces/fixtures.d.ts +8 -0
  130. package/dist/data/traces/fixtures.d.ts.map +1 -0
  131. package/dist/data/traces/har-adapter.cjs +83 -0
  132. package/dist/data/traces/har-adapter.cjs.map +1 -0
  133. package/dist/data/traces/har-adapter.d.ts +13 -0
  134. package/dist/data/traces/har-adapter.d.ts.map +1 -0
  135. package/dist/data/traces/har-adapter.js +83 -0
  136. package/dist/data/traces/har-adapter.js.map +1 -0
  137. package/dist/data/traces/index.d.ts +12 -0
  138. package/dist/data/traces/index.d.ts.map +1 -0
  139. package/dist/data/traces/process-tree.cjs +53 -0
  140. package/dist/data/traces/process-tree.cjs.map +1 -0
  141. package/dist/data/traces/process-tree.d.ts +21 -0
  142. package/dist/data/traces/process-tree.d.ts.map +1 -0
  143. package/dist/data/traces/process-tree.js +53 -0
  144. package/dist/data/traces/process-tree.js.map +1 -0
  145. package/dist/data/traces/sql-trace.cjs +39 -0
  146. package/dist/data/traces/sql-trace.cjs.map +1 -0
  147. package/dist/data/traces/sql-trace.d.ts +31 -0
  148. package/dist/data/traces/sql-trace.d.ts.map +1 -0
  149. package/dist/data/traces/sql-trace.js +39 -0
  150. package/dist/data/traces/sql-trace.js.map +1 -0
  151. package/dist/data/traces/terminal-screen.cjs +270 -0
  152. package/dist/data/traces/terminal-screen.cjs.map +1 -0
  153. package/dist/data/traces/terminal-screen.d.ts +45 -0
  154. package/dist/data/traces/terminal-screen.d.ts.map +1 -0
  155. package/dist/data/traces/terminal-screen.js +270 -0
  156. package/dist/data/traces/terminal-screen.js.map +1 -0
  157. package/dist/data/traces/terminal-timeline.cjs +48 -0
  158. package/dist/data/traces/terminal-timeline.cjs.map +1 -0
  159. package/dist/data/traces/terminal-timeline.d.ts +26 -0
  160. package/dist/data/traces/terminal-timeline.d.ts.map +1 -0
  161. package/dist/data/traces/terminal-timeline.js +48 -0
  162. package/dist/data/traces/terminal-timeline.js.map +1 -0
  163. package/dist/data/traces/types.d.ts +139 -0
  164. package/dist/data/traces/types.d.ts.map +1 -0
  165. package/dist/data/verification/VerificationResults.cjs +4 -0
  166. package/dist/data/verification/VerificationResults.cjs.map +1 -1
  167. package/dist/data/verification/VerificationResults.d.ts +3 -1
  168. package/dist/data/verification/VerificationResults.d.ts.map +1 -1
  169. package/dist/data/verification/VerificationResults.js +4 -0
  170. package/dist/data/verification/VerificationResults.js.map +1 -1
  171. package/dist/data/verification/adapterViews.cjs +12 -3
  172. package/dist/data/verification/adapterViews.cjs.map +1 -1
  173. package/dist/data/verification/adapterViews.d.ts +4 -1
  174. package/dist/data/verification/adapterViews.d.ts.map +1 -1
  175. package/dist/data/verification/adapterViews.js +12 -3
  176. package/dist/data/verification/adapterViews.js.map +1 -1
  177. package/dist/data/verification/adapters.cjs +53 -10
  178. package/dist/data/verification/adapters.cjs.map +1 -1
  179. package/dist/data/verification/adapters.d.ts +7 -1
  180. package/dist/data/verification/adapters.d.ts.map +1 -1
  181. package/dist/data/verification/adapters.js +53 -10
  182. package/dist/data/verification/adapters.js.map +1 -1
  183. package/dist/data/verification/index.d.ts +4 -2
  184. package/dist/data/verification/index.d.ts.map +1 -1
  185. package/dist/data/verification/stepKind.cjs +217 -0
  186. package/dist/data/verification/stepKind.cjs.map +1 -0
  187. package/dist/data/verification/stepKind.d.ts +87 -0
  188. package/dist/data/verification/stepKind.d.ts.map +1 -0
  189. package/dist/data/verification/stepKind.js +217 -0
  190. package/dist/data/verification/stepKind.js.map +1 -0
  191. package/dist/data/verification/stepKindViews.cjs +111 -0
  192. package/dist/data/verification/stepKindViews.cjs.map +1 -0
  193. package/dist/data/verification/stepKindViews.d.ts +18 -0
  194. package/dist/data/verification/stepKindViews.d.ts.map +1 -0
  195. package/dist/data/verification/stepKindViews.js +111 -0
  196. package/dist/data/verification/stepKindViews.js.map +1 -0
  197. package/dist/data/verification/verifyReportTests.cjs +1 -0
  198. package/dist/data/verification/verifyReportTests.cjs.map +1 -1
  199. package/dist/data/verification/verifyReportTests.d.ts +2 -1
  200. package/dist/data/verification/verifyReportTests.d.ts.map +1 -1
  201. package/dist/data/verification/verifyReportTests.js +1 -0
  202. package/dist/data/verification/verifyReportTests.js.map +1 -1
  203. package/dist/data/version-info.cjs +3 -3
  204. package/dist/data/version-info.cjs.map +1 -1
  205. package/dist/data/version-info.js +3 -3
  206. package/dist/data/version-info.js.map +1 -1
  207. package/dist/data.cjs +36 -0
  208. package/dist/data.cjs.map +1 -1
  209. package/dist/data.d.ts +2 -0
  210. package/dist/data.d.ts.map +1 -1
  211. package/dist/data.js +38 -2
  212. package/dist/data.js.map +1 -1
  213. package/dist/index.cjs +38 -0
  214. package/dist/index.cjs.map +1 -1
  215. package/dist/index.js +40 -2
  216. package/dist/index.js.map +1 -1
  217. package/dist/rpc/FilterForm.cjs +9 -0
  218. package/dist/rpc/FilterForm.cjs.map +1 -1
  219. package/dist/rpc/FilterForm.d.ts +5 -1
  220. package/dist/rpc/FilterForm.d.ts.map +1 -1
  221. package/dist/rpc/FilterForm.js +9 -0
  222. package/dist/rpc/FilterForm.js.map +1 -1
  223. package/dist/rpc/OperationArgsForm.cjs +153 -0
  224. package/dist/rpc/OperationArgsForm.cjs.map +1 -0
  225. package/dist/rpc/OperationArgsForm.d.ts +16 -0
  226. package/dist/rpc/OperationArgsForm.d.ts.map +1 -0
  227. package/dist/rpc/OperationArgsForm.js +153 -0
  228. package/dist/rpc/OperationArgsForm.js.map +1 -0
  229. package/dist/rpc/OperationScheduleDialog.cjs +61 -17
  230. package/dist/rpc/OperationScheduleDialog.cjs.map +1 -1
  231. package/dist/rpc/OperationScheduleDialog.d.ts +3 -0
  232. package/dist/rpc/OperationScheduleDialog.d.ts.map +1 -1
  233. package/dist/rpc/OperationScheduleDialog.js +62 -18
  234. package/dist/rpc/OperationScheduleDialog.js.map +1 -1
  235. package/dist/rpc/OperationSchedules.cjs +6 -1
  236. package/dist/rpc/OperationSchedules.cjs.map +1 -1
  237. package/dist/rpc/OperationSchedules.d.ts.map +1 -1
  238. package/dist/rpc/OperationSchedules.js +6 -1
  239. package/dist/rpc/OperationSchedules.js.map +1 -1
  240. package/dist/rpc/operation-schedule-model.cjs +0 -7
  241. package/dist/rpc/operation-schedule-model.cjs.map +1 -1
  242. package/dist/rpc/operation-schedule-model.d.ts +0 -2
  243. package/dist/rpc/operation-schedule-model.d.ts.map +1 -1
  244. package/dist/rpc/operation-schedule-model.js +0 -7
  245. package/dist/rpc/operation-schedule-model.js.map +1 -1
  246. package/dist/rpc/operation-schedule-types.d.ts +6 -1
  247. package/dist/rpc/operation-schedule-types.d.ts.map +1 -1
  248. package/dist/rpc.cjs +2 -0
  249. package/dist/rpc.cjs.map +1 -1
  250. package/dist/rpc.d.ts +1 -0
  251. package/dist/rpc.d.ts.map +1 -1
  252. package/dist/rpc.js +2 -0
  253. package/dist/rpc.js.map +1 -1
  254. package/dist/styles.css +1 -1
  255. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TaskProgress.js","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { taskWorkProgress } from \"./task-work-progress\";\nimport { useState } from \"react\";\nimport { Button } from \"../components/button\";\nimport { CopyButton } from \"../components/CopyButton\";\nimport { SplitButton } from \"../components/SplitButton\";\nimport { useCopyFlash } from \"../components/clipboard\";\nimport { UiCheck, UiCopy, UiJson, UiPlay, UiRefresh, UiRestart, UiStop, UiWarningTriangle } from \"../icons\";\nimport { cn } from \"../lib/utils\";\nimport type { DropdownMenuItem } from \"../overlay/DropdownMenu\";\nimport { Icon } from \"./Icon\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProcessDetailsView } from \"./TaskProcessDetails\";\nimport { TaskExecDetailsView } from \"./TaskExecDetails\";\nimport type { LogEntry, TaskControlAction, TaskSnapshot } from \"./TaskSnapshot\";\nimport { type TaskHeaderExtra, TaskMetadataChips } from \"./TaskHeaderExtras\";\nimport { type TaskExtraTabs, TaskStreamTabs } from \"./TaskStreamTabs\";\nimport {\n isFailedOrWarn,\n taskGroupErrorCount,\n taskGroupErrors,\n taskGroupJson,\n taskGroupMarkdown,\n taskMarkdown,\n} from \"./task-copy\";\nimport { isTaskExecDetails } from \"./task-exec-details\";\nimport { isTaskProcessDetails } from \"./task-process-details\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n className?: string;\n onControl?: (action: TaskControlAction, group: TaskSnapshot) => void | Promise<void>;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n /**\n * Contributes host-owned output panes alongside a task's own stdout/stderr —\n * an agent's session transcript, say, which this module has no way to fetch.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default, which chips the scalar values of its\n * process metadata.\n */\n headerExtra?: TaskHeaderExtra;\n}\n\nexport function TaskProgress({\n snapshots,\n title,\n compact,\n className,\n onControl,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs,\n headerExtra,\n}: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => g.groupId ? t.groupId === g.groupId : t.group === g.id)}\n compact={compact}\n {...(onControl ? { onControl } : {})}\n {...(onTaskControl ? { onTaskControl } : {})}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n ))}\n </div>\n );\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n onControl,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs,\n headerExtra,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n extraTabs?: TaskExtraTabs;\n headerExtra?: TaskHeaderExtra;\n onControl?: (action: TaskControlAction, group: TaskSnapshot) => void | Promise<void>;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n}) {\n const [showAll, setShowAll] = useState(false);\n const { state: copyState, copy } = useCopyFlash();\n const work = taskWorkProgress(g);\n const total = g.work ? work.total : g.total ?? tasks.length;\n const counts = g.work ? work.counts : bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail + counts.canceled;\n const progress = g.work ? work.label : total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n const copyItems: DropdownMenuItem[] = [\n {\n label: \"Copy as JSON\",\n icon: UiJson,\n onSelect: () => copy(taskGroupJson(g, tasks)),\n },\n {\n label: \"Copy errors only\",\n icon: UiWarningTriangle,\n disabled: taskGroupErrorCount(tasks) === 0,\n onSelect: () => copy(taskGroupErrors(g, tasks)),\n },\n ];\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-3\" : \"p-4\")}>\n <div className=\"mb-3 flex items-center justify-between gap-3\">\n <h3 className=\"flex min-w-0 items-center gap-1.5 text-sm font-semibold\">\n <Icon\n icon={taskStatusIcon(g.status)}\n className={cn(\n \"shrink-0\",\n taskStatusColor(g.status),\n g.status === \"running\" && \"animate-spin\",\n )}\n />\n <span className=\"truncate\">{g.name}</span>\n {progress && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{progress}</span>\n )}\n {g.kind && (\n <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n <div className=\"flex shrink-0 items-center gap-1\">\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n <SplitButton\n label={copyState === \"copied\" ? \"Copied\" : copyState === \"error\" ? \"Copy failed\" : \"Copy\"}\n icon={copyState === \"copied\" ? UiCheck : UiCopy}\n variant=\"ghost\"\n size=\"sm\"\n title=\"More copy options\"\n onClick={() => copy(taskGroupMarkdown(g, tasks))}\n items={copyItems}\n />\n {onControl && <TaskControls target={g} onControl={onControl} />}\n </div>\n </div>\n\n {total > 0 && (\n <div className=\"mb-3\">\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {isTaskProcessDetails(g.details) && (\n <TaskProcessDetailsView\n details={g.details}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n />\n )}\n\n {!isTaskProcessDetails(g.details) && isTaskExecDetails(g.details) && (\n <TaskExecDetailsView details={g.details} />\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className=\"border-b py-1.5 text-xs text-muted-foreground hover:text-foreground\"\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow\n key={t.id}\n task={t}\n group={g}\n {...(onTaskControl ? { onTaskControl } : {})}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({\n task: t,\n group,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs: contributeTabs,\n headerExtra,\n}: {\n task: TaskSnapshot;\n group: TaskSnapshot;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n extraTabs?: TaskExtraTabs;\n headerExtra?: TaskHeaderExtra;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n}) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n const extraTabs = contributeTabs?.(t, group) ?? [];\n // A host that has nothing to add for this task returns null, and the default\n // metadata chips take over — so enriching one kind of task never costs the\n // rest their header.\n const header = headerExtra?.(t, group) ?? <TaskMetadataChips task={t} />;\n const hasOutput = !!t.stdout || !!t.stderr || extraTabs.length > 0;\n const execDetails = isTaskExecDetails(t.details) ? t.details : undefined;\n const processDetails = isTaskProcessDetails(t.details) ? t.details : undefined;\n const expandable = hasLogs || hasOutput || execDetails !== undefined || processDetails !== undefined;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n // Bounded per-task progress (e.g. records emitted in a phase): render an x/y\n // count, a percentage, and a thin bar while the task runs.\n const done = t.progress ?? 0;\n // Only surface bounded progress once work has actually started — an untouched\n // task should not render a \"0/100 · 0%\" placeholder.\n const hasProgress = typeof t.maxValue === \"number\" && t.maxValue > 0 && done > 0;\n const pct = hasProgress ? Math.round((done / (t.maxValue as number)) * 100) : 0;\n\n return (\n <div\n className={cn(\n \"group flex items-start gap-3 border-b py-2 last:border-0\",\n expandable && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={expandable ? () => setExpanded((v) => !v) : undefined}\n >\n <Icon\n icon={taskStatusIcon(t.status)}\n className={cn(taskStatusColor(t.status), \"mt-0.5 text-lg\", t.status === \"running\" && \"animate-spin\")}\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {expandable && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length + (t.stdout ? 1 : 0) + (t.stderr ? 1 : 0) + (execDetails || processDetails ? 1 : 0)}\n </span>\n )}\n </span>\n {header}\n <div className=\"flex shrink-0 items-center gap-1\">\n {t.duration && <span className=\"text-xs text-muted-foreground\">{t.duration}</span>}\n <CopyButton\n value={() => taskMarkdown(t)}\n label={`Copy ${t.name}`}\n className=\"opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n iconClassName=\"text-xs\"\n />\n {onTaskControl && (\n <TaskControls\n target={t}\n labelSuffix={t.name}\n stopPropagation\n onControl={(action) => onTaskControl(action, t, group)}\n />\n )}\n </div>\n </div>\n {(t.description || hasProgress) && (\n <div className=\"mt-0.5 flex items-center gap-2\">\n {t.description && (\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">{t.description}</span>\n )}\n {hasProgress && (\n <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n {done}/{t.maxValue} · {pct}%\n </span>\n )}\n </div>\n )}\n {hasProgress && t.status === \"running\" && (\n <div className=\"mt-1\">\n <ProgressBar\n segments={[{ count: done, color: \"bg-blue-500\", label: \"done\" }]}\n total={t.maxValue as number}\n height=\"h-1\"\n />\n </div>\n )}\n {expanded && execDetails && <TaskExecDetailsView details={execDetails} />}\n {expanded && processDetails && (\n <TaskProcessDetailsView\n details={processDetails}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n />\n )}\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n {expanded && <TaskStreamTabs task={t} {...(extraTabs.length > 0 ? { extraTabs } : {})} />}\n </div>\n </div>\n );\n}\n\nfunction TaskControls({\n target,\n onControl,\n labelSuffix,\n stopPropagation,\n}: {\n target: TaskSnapshot;\n onControl: (action: TaskControlAction, target: TaskSnapshot) => void | Promise<void>;\n labelSuffix?: string;\n stopPropagation?: boolean;\n}) {\n const [busy, setBusy] = useState<TaskControlAction | null>(null);\n const [error, setError] = useState(\"\");\n const icons: Record<TaskControlAction, typeof UiPlay> = {\n start: UiPlay,\n stop: UiStop,\n restart: UiRestart,\n drain: UiStop,\n retry: UiRefresh,\n };\n const invoke = async (action: TaskControlAction) => {\n setBusy(action);\n setError(\"\");\n try {\n await onControl(action, target);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : `Failed to ${action} task`);\n } finally {\n setBusy(null);\n }\n };\n return (\n <>\n {(target.controls ?? []).map((action) => {\n const ControlIcon = icons[action];\n const actionLabel = action === \"drain\" ? \"Stop after current\" : action[0]?.toUpperCase() + action.slice(1);\n const label = labelSuffix ? `${actionLabel} ${labelSuffix}` : actionLabel;\n return (\n <Button\n key={action}\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={label}\n title={label}\n disabled={busy !== null}\n onClick={(event) => {\n if (stopPropagation) event.stopPropagation();\n void invoke(action);\n }}\n >\n <ControlIcon />\n </Button>\n );\n })}\n {error && <span role=\"alert\" className=\"text-xs text-red-600\">{error}</span>}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TaskProgress.js","sources":["../../src/data/TaskProgress.tsx"],"sourcesContent":["import { taskWorkProgress } from \"./task-work-progress\";\nimport { useState } from \"react\";\nimport { Button } from \"../components/button\";\nimport { CopyButton } from \"../components/CopyButton\";\nimport { SplitButton } from \"../components/SplitButton\";\nimport { useCopyFlash } from \"../components/clipboard\";\nimport { UiCheck, UiCopy, UiJson, UiPlay, UiRefresh, UiRestart, UiStop, UiWarningTriangle } from \"../icons\";\nimport { cn } from \"../lib/utils\";\nimport type { DropdownMenuItem } from \"../overlay/DropdownMenu\";\nimport { Icon } from \"./Icon\";\nimport { ProgressBar } from \"./ProgressBar\";\nimport { TaskProcessDetailsView } from \"./TaskProcessDetails\";\nimport { TaskExecDetailsView } from \"./TaskExecDetails\";\nimport type { LogEntry, TaskControlAction, TaskSnapshot } from \"./TaskSnapshot\";\nimport { type TaskHeaderExtra, TaskMetadataChips } from \"./TaskHeaderExtras\";\nimport { type TaskExtraTabs, TaskStreamTabs } from \"./TaskStreamTabs\";\nimport {\n isFailedOrWarn,\n taskGroupErrorCount,\n taskGroupErrors,\n taskGroupJson,\n taskGroupMarkdown,\n taskMarkdown,\n} from \"./task-copy\";\nimport { isTaskExecDetails } from \"./task-exec-details\";\nimport { isTaskProcessDetails } from \"./task-process-details\";\nimport {\n bucketTasks,\n logLevelColor,\n taskSegments,\n taskStatusBg,\n taskStatusColor,\n taskStatusIcon,\n} from \"./task-status\";\n\n// TaskProgress renders one or more clicky task runs (groups) and their child\n// tasks: a segmented progress bar per group plus collapsible per-task rows with\n// status icon, duration, error, and expandable logs. Presentational only —\n// callers feed it snapshots from useTaskRun (SSE) or any other source.\n\nconst MAX_COMPLETED = 5;\nconst MAX_PENDING = 3;\n\nexport interface TaskProgressProps {\n snapshots: TaskSnapshot[];\n title?: string;\n /** Tighter spacing for embedding in a panel. */\n compact?: boolean;\n /** Show every child task when the group first opens. */\n initialShowAllTasks?: boolean;\n className?: string;\n onControl?: (action: TaskControlAction, group: TaskSnapshot) => void | Promise<void>;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n /**\n * Contributes host-owned output panes alongside a task's own stdout/stderr —\n * an agent's session transcript, say, which this module has no way to fetch.\n */\n extraTabs?: TaskExtraTabs;\n /**\n * Replaces the content beside a task's name in its header row. Returning null\n * for a task falls back to the default, which chips the scalar values of its\n * process metadata.\n */\n headerExtra?: TaskHeaderExtra;\n}\n\nexport function TaskProgress({\n snapshots,\n title,\n compact,\n initialShowAllTasks,\n className,\n onControl,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs,\n headerExtra,\n}: TaskProgressProps) {\n const groups = snapshots.filter((s) => s.type === \"group\");\n const tasks = snapshots.filter((s) => s.type === \"task\");\n\n if (groups.length === 0) {\n return (\n <div className={cn(\"text-sm text-muted-foreground\", className)}>\n {title ? `${title}: ` : \"\"}No tasks yet.\n </div>\n );\n }\n\n return (\n <div className={cn(compact ? \"space-y-2\" : \"space-y-3\", className)}>\n {title && <h2 className=\"text-sm font-semibold\">{title}</h2>}\n {groups.map((g) => (\n <TaskGroupCard\n key={g.groupId || g.id}\n group={g}\n tasks={tasks.filter((t) => g.groupId ? t.groupId === g.groupId : t.group === g.id)}\n compact={compact}\n initialShowAllTasks={initialShowAllTasks}\n {...(onControl ? { onControl } : {})}\n {...(onTaskControl ? { onTaskControl } : {})}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n ))}\n </div>\n );\n}\n\nfunction TaskGroupCard({\n group: g,\n tasks,\n compact,\n initialShowAllTasks,\n onControl,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs,\n headerExtra,\n}: {\n group: TaskSnapshot;\n tasks: TaskSnapshot[];\n compact: boolean | undefined;\n initialShowAllTasks: boolean | undefined;\n extraTabs?: TaskExtraTabs;\n headerExtra?: TaskHeaderExtra;\n onControl?: (action: TaskControlAction, group: TaskSnapshot) => void | Promise<void>;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n}) {\n const [showAll, setShowAll] = useState(initialShowAllTasks ?? false);\n const { state: copyState, copy } = useCopyFlash();\n const work = taskWorkProgress(g);\n const total = g.work ? work.total : g.total ?? tasks.length;\n const counts = g.work ? work.counts : bucketTasks(tasks);\n const done = counts.ok + counts.warn + counts.fail + counts.canceled;\n const progress = g.work ? work.label : total > 0 ? `${done}/${total}` : \"\";\n\n const running = tasks.filter((t) => t.status === \"running\");\n const pending = tasks.filter((t) => t.status === \"pending\");\n const completed = tasks.filter((t) => t.status !== \"running\" && t.status !== \"pending\");\n const alwaysShow = completed.filter(isFailedOrWarn);\n const collapsible = completed.filter((t) => !isFailedOrWarn(t));\n\n const hiddenCompleted = collapsible.length - MAX_COMPLETED;\n const collapseCompleted = !showAll && hiddenCompleted > 0;\n const visibleSuccess = collapseCompleted ? collapsible.slice(-MAX_COMPLETED) : collapsible;\n\n const hiddenPending = pending.length - MAX_PENDING;\n const collapsePending = !showAll && hiddenPending > 0;\n const visiblePending = collapsePending ? pending.slice(0, MAX_PENDING) : pending;\n\n const isTerminal = g.status !== \"running\" && g.status !== \"pending\";\n\n const copyItems: DropdownMenuItem[] = [\n {\n label: \"Copy as JSON\",\n icon: UiJson,\n onSelect: () => copy(taskGroupJson(g, tasks)),\n },\n {\n label: \"Copy errors only\",\n icon: UiWarningTriangle,\n disabled: taskGroupErrorCount(tasks) === 0,\n onSelect: () => copy(taskGroupErrors(g, tasks)),\n },\n ];\n\n return (\n <div className={cn(\"rounded-lg border bg-card\", compact ? \"p-2\" : \"p-4\")}>\n <div className={cn(\"flex items-center justify-between gap-3\", compact ? \"mb-2\" : \"mb-3\")}>\n <h3 className=\"flex min-w-0 items-center gap-1.5 text-sm font-semibold\">\n <Icon\n icon={taskStatusIcon(g.status)}\n className={cn(\n \"shrink-0\",\n taskStatusColor(g.status),\n g.status === \"running\" && \"animate-spin\",\n )}\n />\n <span className=\"truncate\">{g.name}</span>\n {progress && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{progress}</span>\n )}\n {g.kind && (\n <span className=\"shrink-0 rounded-full bg-muted px-1.5 py-0 text-[10px] text-muted-foreground\">\n {g.kind}\n </span>\n )}\n </h3>\n <div className=\"flex shrink-0 items-center gap-1\">\n {isTerminal && (\n <span\n className={cn(\n \"rounded-full px-2 py-0.5 text-xs\",\n taskStatusColor(g.status),\n taskStatusBg(g.status),\n )}\n >\n {g.status}\n </span>\n )}\n <SplitButton\n label={copyState === \"copied\" ? \"Copied\" : copyState === \"error\" ? \"Copy failed\" : \"Copy\"}\n icon={copyState === \"copied\" ? UiCheck : UiCopy}\n variant=\"ghost\"\n size=\"sm\"\n title=\"More copy options\"\n onClick={() => copy(taskGroupMarkdown(g, tasks))}\n items={copyItems}\n />\n {onControl && <TaskControls target={g} onControl={onControl} />}\n </div>\n </div>\n\n {total > 0 && (\n <div className={compact ? \"mb-2\" : \"mb-3\"}>\n <ProgressBar segments={taskSegments(counts)} total={total} height=\"h-1.5\" />\n </div>\n )}\n\n {isTaskProcessDetails(g.details) && (\n <TaskProcessDetailsView\n details={g.details}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n />\n )}\n\n {!isTaskProcessDetails(g.details) && isTaskExecDetails(g.details) && (\n <TaskExecDetailsView details={g.details} />\n )}\n\n {hiddenCompleted > 0 || hiddenPending > 0 ? (\n <button\n type=\"button\"\n className={cn(\"border-b text-xs text-muted-foreground hover:text-foreground\", compact ? \"py-1\" : \"py-1.5\")}\n onClick={() => setShowAll((v) => !v)}\n >\n {showAll\n ? \"▲ collapse\"\n : `... ${(collapseCompleted ? hiddenCompleted : 0) + (collapsePending ? hiddenPending : 0)} more tasks`}\n </button>\n ) : null}\n\n {[...visibleSuccess, ...alwaysShow, ...running, ...visiblePending].map((t) => (\n <TaskRow\n key={t.id}\n task={t}\n group={g}\n compact={compact}\n {...(onTaskControl ? { onTaskControl } : {})}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n {...(extraTabs ? { extraTabs } : {})}\n {...(headerExtra ? { headerExtra } : {})}\n />\n ))}\n </div>\n );\n}\n\nfunction TaskRow({\n task: t,\n group,\n compact,\n onTaskControl,\n metricsBaseUrl,\n connectionStatus,\n extraTabs: contributeTabs,\n headerExtra,\n}: {\n task: TaskSnapshot;\n group: TaskSnapshot;\n compact: boolean | undefined;\n metricsBaseUrl?: string;\n connectionStatus?: string;\n extraTabs?: TaskExtraTabs;\n headerExtra?: TaskHeaderExtra;\n onTaskControl?: (\n action: TaskControlAction,\n task: TaskSnapshot,\n group: TaskSnapshot,\n ) => void | Promise<void>;\n}) {\n const [expanded, setExpanded] = useState(false);\n const logs: LogEntry[] = t.logs ?? [];\n const hasLogs = logs.length > 0;\n const extraTabs = contributeTabs?.(t, group) ?? [];\n // A host that has nothing to add for this task returns null, and the default\n // metadata chips take over — so enriching one kind of task never costs the\n // rest their header.\n const header = headerExtra?.(t, group) ?? <TaskMetadataChips task={t} />;\n const hasOutput = !!t.stdout || !!t.stderr || extraTabs.length > 0;\n const execDetails = isTaskExecDetails(t.details) ? t.details : undefined;\n const processDetails = isTaskProcessDetails(t.details) ? t.details : undefined;\n const expandable = hasLogs || hasOutput || execDetails !== undefined || processDetails !== undefined;\n // Promote the latest warning message inline so a `warning` row shows its\n // reason without expanding. Suppressed when an error is already shown.\n const latestWarn = t.error ? undefined : logs.filter((l) => l.level === \"warn\").at(-1);\n // Bounded per-task progress (e.g. records emitted in a phase): render an x/y\n // count, a percentage, and a thin bar while the task runs.\n const done = t.progress ?? 0;\n // Only surface bounded progress once work has actually started — an untouched\n // task should not render a \"0/100 · 0%\" placeholder.\n const hasProgress = typeof t.maxValue === \"number\" && t.maxValue > 0 && done > 0;\n const pct = hasProgress ? Math.round((done / (t.maxValue as number)) * 100) : 0;\n\n return (\n <div\n className={cn(\n \"group flex items-start border-b last:border-0\",\n compact ? \"gap-2 py-1\" : \"gap-3 py-2\",\n expandable && \"-mx-1 cursor-pointer rounded px-1 hover:bg-muted/50\",\n )}\n onClick={expandable ? () => setExpanded((v) => !v) : undefined}\n >\n <Icon\n icon={taskStatusIcon(t.status)}\n className={cn(taskStatusColor(t.status), \"mt-0.5\", compact ? \"text-base\" : \"text-lg\", t.status === \"running\" && \"animate-spin\")}\n />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex min-w-0 items-center gap-1.5 text-sm font-medium\">\n <span className=\"truncate\">{t.name}</span>\n {t.error && <span className=\"truncate text-xs font-normal text-red-500\">{t.error}</span>}\n {latestWarn && (\n <span className={cn(\"truncate text-xs font-normal\", logLevelColor(\"warn\"))}>\n {latestWarn.message}\n </span>\n )}\n {expandable && (\n <span className=\"inline-flex shrink-0 items-center rounded-full bg-muted px-1.5 text-[10px] font-medium text-muted-foreground\">\n {logs.length + (t.stdout ? 1 : 0) + (t.stderr ? 1 : 0) + (execDetails || processDetails ? 1 : 0)}\n </span>\n )}\n </span>\n {header}\n <div className=\"flex shrink-0 items-center gap-1\">\n {t.duration && <span className=\"text-xs text-muted-foreground\">{t.duration}</span>}\n <CopyButton\n value={() => taskMarkdown(t)}\n label={`Copy ${t.name}`}\n className=\"opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100\"\n iconClassName=\"text-xs\"\n />\n {onTaskControl && (\n <TaskControls\n target={t}\n labelSuffix={t.name}\n stopPropagation\n onControl={(action) => onTaskControl(action, t, group)}\n />\n )}\n </div>\n </div>\n {(t.description || hasProgress) && (\n <div className=\"mt-0.5 flex items-center gap-2\">\n {t.description && (\n <span className=\"min-w-0 flex-1 truncate text-xs text-muted-foreground\">{t.description}</span>\n )}\n {hasProgress && (\n <span className=\"shrink-0 text-xs tabular-nums text-muted-foreground\">\n {done}/{t.maxValue} · {pct}%\n </span>\n )}\n </div>\n )}\n {hasProgress && t.status === \"running\" && (\n <div className=\"mt-1\">\n <ProgressBar\n segments={[{ count: done, color: \"bg-blue-500\", label: \"done\" }]}\n total={t.maxValue as number}\n height=\"h-1\"\n />\n </div>\n )}\n {expanded && execDetails && <TaskExecDetailsView details={execDetails} />}\n {expanded && processDetails && (\n <TaskProcessDetailsView\n details={processDetails}\n {...(metricsBaseUrl ? { metricsBaseUrl } : {})}\n {...(connectionStatus ? { connectionStatus } : {})}\n />\n )}\n {expanded && hasLogs && (\n <div className=\"mt-1 ml-1 max-h-48 space-y-0.5 overflow-y-auto border-l-2 pl-2\">\n {logs.map((l, i) => (\n <div key={i} className={cn(\"text-xs\", logLevelColor(l.level))}>\n <span className=\"mr-1 font-mono text-muted-foreground\">{l.level.substring(0, 3)}</span>\n {l.message}\n </div>\n ))}\n </div>\n )}\n {expanded && <TaskStreamTabs task={t} {...(extraTabs.length > 0 ? { extraTabs } : {})} />}\n </div>\n </div>\n );\n}\n\nfunction TaskControls({\n target,\n onControl,\n labelSuffix,\n stopPropagation,\n}: {\n target: TaskSnapshot;\n onControl: (action: TaskControlAction, target: TaskSnapshot) => void | Promise<void>;\n labelSuffix?: string;\n stopPropagation?: boolean;\n}) {\n const [busy, setBusy] = useState<TaskControlAction | null>(null);\n const [error, setError] = useState(\"\");\n const icons: Record<TaskControlAction, typeof UiPlay> = {\n start: UiPlay,\n stop: UiStop,\n restart: UiRestart,\n drain: UiStop,\n retry: UiRefresh,\n };\n const invoke = async (action: TaskControlAction) => {\n setBusy(action);\n setError(\"\");\n try {\n await onControl(action, target);\n } catch (cause) {\n setError(cause instanceof Error ? cause.message : `Failed to ${action} task`);\n } finally {\n setBusy(null);\n }\n };\n return (\n <>\n {(target.controls ?? []).map((action) => {\n const ControlIcon = icons[action];\n const actionLabel = action === \"drain\" ? \"Stop after current\" : action[0]?.toUpperCase() + action.slice(1);\n const label = labelSuffix ? `${actionLabel} ${labelSuffix}` : actionLabel;\n return (\n <Button\n key={action}\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n aria-label={label}\n title={label}\n disabled={busy !== null}\n onClick={(event) => {\n if (stopPropagation) event.stopPropagation();\n void invoke(action);\n }}\n >\n <ControlIcon />\n </Button>\n );\n })}\n {error && <span role=\"alert\" className=\"text-xs text-red-600\">{error}</span>}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ const jsxRuntime = require("react/jsx-runtime");
4
4
  const react = require("react");
5
5
  const Icon = require("../Icon.cjs");
6
6
  const button = require("../../components/button.cjs");
7
+ const select = require("../../components/select.cjs");
7
8
  const SplitButton = require("../../components/SplitButton.cjs");
8
9
  const clipboard = require("../../components/clipboard.cjs");
9
10
  const utils = require("../../lib/utils.cjs");
@@ -12,6 +13,7 @@ const TabButton = require("../TabButton.cjs");
12
13
  const context = require("./context.cjs");
13
14
  const copy = require("./copy.cjs");
14
15
  const status = require("./status.cjs");
16
+ const marks = require("./marks.cjs");
15
17
  const MarkIcons = require("./MarkIcons.cjs");
16
18
  const TestFailureDetail = require("./TestFailureDetail.cjs");
17
19
  const UiClass = require("../../icons/components/UiClass.cjs");
@@ -87,11 +89,11 @@ function CopyDetailsButton({ node }) {
87
89
  }
88
90
  );
89
91
  }
90
- function DetailHeader({ node, actions }) {
92
+ function DetailHeader({ node, actionNode, actions, attemptChooser }) {
91
93
  const runner = context.useTestRunner();
92
94
  const outcome = status.outcomeFor(node);
93
- const canRerun = !!runner.onRerun && node.framework !== "task";
94
- const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;
95
+ const canRerun = !!runner.onRerun && actionNode.framework !== "task";
96
+ const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;
95
97
  const location = node.file ? `${node.file}${node.line ? `:${node.line}` : ""}` : void 0;
96
98
  return /* @__PURE__ */ jsxRuntime.jsxs(
97
99
  "div",
@@ -102,7 +104,7 @@ function DetailHeader({ node, actions }) {
102
104
  outcome ? /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.OutcomeIcon, { outcome, className: "text-lg" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiClass.UiClass, className: "shrink-0 text-lg text-muted-foreground" }),
103
105
  /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "min-w-0 shrink truncate text-base font-semibold", title: node.name, children: node.name }),
104
106
  node.framework && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground", children: [
105
- /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.ToolIcon, { tool: node.framework, className: "text-sm" }),
107
+ /* @__PURE__ */ jsxRuntime.jsx(MarkIcons.ToolIcon, { tool: marks.toolKeyFor(node), className: "text-sm" }),
106
108
  node.framework
107
109
  ] }),
108
110
  node.duration ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground", children: [
@@ -118,6 +120,7 @@ function DetailHeader({ node, actions }) {
118
120
  }
119
121
  ),
120
122
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [
123
+ attemptChooser,
121
124
  actions,
122
125
  /* @__PURE__ */ jsxRuntime.jsx(CopyDetailsButton, { node }),
123
126
  canRerun && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -125,7 +128,7 @@ function DetailHeader({ node, actions }) {
125
128
  {
126
129
  size: "sm",
127
130
  disabled: runner.busy.rerun,
128
- onClick: () => runner.onRerun(node),
131
+ onClick: () => runner.onRerun(actionNode),
129
132
  title: "Rerun this test or subtree",
130
133
  children: [
131
134
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiRefresh.UiRefresh, className: "mr-1 text-sm" }),
@@ -139,7 +142,7 @@ function DetailHeader({ node, actions }) {
139
142
  size: "sm",
140
143
  variant: "destructive",
141
144
  disabled: runner.busy.stop,
142
- onClick: () => runner.onStop(node),
145
+ onClick: () => runner.onStop(actionNode),
143
146
  title: "Stop this running task",
144
147
  children: [
145
148
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiStop.UiStop, className: "mr-1 text-sm" }),
@@ -155,7 +158,8 @@ function DetailHeader({ node, actions }) {
155
158
  function TestDetailPanel() {
156
159
  var _a;
157
160
  const runner = context.useTestRunner();
158
- const node = runner.selected;
161
+ const selected = runner.selected;
162
+ const [attemptSelection, setAttemptSelection] = react.useState(null);
159
163
  const controlled = runner.activeTab !== void 0;
160
164
  const [internalTab, setInternalTab] = react.useState("detail");
161
165
  const activeTab = controlled ? runner.activeTab : internalTab;
@@ -164,22 +168,32 @@ function TestDetailPanel() {
164
168
  if (controlled) (_a2 = runner.onTabChange) == null ? void 0 : _a2.call(runner, id);
165
169
  else setInternalTab(id);
166
170
  };
167
- if (!node) {
171
+ if (!selected) {
168
172
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
169
173
  /* @__PURE__ */ jsxRuntime.jsx(Icon.Icon, { icon: UiClass.UiClass, className: "mb-2 block text-4xl" }),
170
174
  "Select a test to view its details."
171
175
  ] }) });
172
176
  }
177
+ const attempts = runner.adapters.resolveAttempts(selected, runner);
178
+ const key = selected.task_id ?? selected.name;
179
+ const attemptId = runner.activeAttempt ?? ((attemptSelection == null ? void 0 : attemptSelection.key) === key ? attemptSelection.id : void 0);
180
+ const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);
181
+ const node = attempts.length > 1 && attempt ? attempt.node : selected;
182
+ const attemptChooser = attempts.length > 1 && attempt ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-44 shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx(select.Select, { "aria-label": "Attempt", value: String(attempt.id), className: "h-7 text-xs", onChange: (event) => {
183
+ const id = Number(event.target.value);
184
+ if (runner.onAttemptChange) runner.onAttemptChange(id);
185
+ else setAttemptSelection({ key, id });
186
+ }, options: attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== void 0 ? ` · ${status.formatTestDuration(a.durationMs * 1e6)}` : ""}` })) }) }) : null;
173
187
  const detailAdapter = runner.adapters.resolveDetail(node, runner);
174
- const actionAdapter = runner.adapters.resolveActions(node, runner);
188
+ const actionAdapter = runner.adapters.resolveActions(selected, runner);
175
189
  const tabs = runner.adapters.resolveTabs(node, runner);
176
190
  const ownsScroll = runner.adapters.ownsScroll(node, runner);
177
191
  const body = (detailAdapter == null ? void 0 : detailAdapter.renderDetail) ? detailAdapter.renderDetail({ node, runner }) : /* @__PURE__ */ jsxRuntime.jsx(DefaultDetailBody, { node });
178
- const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node, runner });
192
+ const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node: selected, runner });
179
193
  const detailRegion = utils.cn("min-h-0 flex-1 [container-type:size]", !ownsScroll && "overflow-y-auto");
180
194
  if (tabs.length === 0) {
181
195
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
182
- /* @__PURE__ */ jsxRuntime.jsx(DetailHeader, { node, actions }),
196
+ /* @__PURE__ */ jsxRuntime.jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
183
197
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: detailRegion, children: body })
184
198
  ] });
185
199
  }
@@ -187,7 +201,7 @@ function TestDetailPanel() {
187
201
  const allTabs = [detailTab, ...tabs];
188
202
  const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;
189
203
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
190
- /* @__PURE__ */ jsxRuntime.jsx(DetailHeader, { node, actions }),
204
+ /* @__PURE__ */ jsxRuntime.jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
191
205
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1", children: allTabs.map((t) => /* @__PURE__ */ jsxRuntime.jsx(
192
206
  TabButton.TabButton,
193
207
  {
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.cjs","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actions }: { node: Test; actions?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={node.framework} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(node)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(node)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const node = runner.selected;\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!node) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(node, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["sum","jsxs","jsx","TestFailureDetail","TestOutput","TestAttempts","JsonView","copy","UiCopy","UiCheck","UiWarningCircle","useTestRunner","useCopyFlash","useMemo","UiJson","buildNodeJson","SplitButton","buildNodeBrief","outcomeFor","OutcomeIcon","Icon","UiClass","ToolIcon","UiClock","formatTestDuration","Button","UiRefresh","UiStop","useState","_a","cn","TabButton"],"mappings":"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"}
1
+ {"version":3,"file":"TestDetailPanel.cjs","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { Select } from \"../../components/select\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { toolKeyFor } from \"./marks\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actionNode, actions, attemptChooser }: { node: Test; actionNode: Test; actions?: ReactNode; attemptChooser?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && actionNode.framework !== \"task\";\n const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={toolKeyFor(node)} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {attemptChooser}\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(actionNode)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(actionNode)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const selected = runner.selected;\n const [attemptSelection, setAttemptSelection] = useState<{ key: string; id: number } | null>(null);\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!selected) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const attempts = runner.adapters.resolveAttempts(selected, runner);\n const key = selected.task_id ?? selected.name;\n const attemptId = runner.activeAttempt ?? (attemptSelection?.key === key ? attemptSelection.id : undefined);\n const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);\n const node = attempts.length > 1 && attempt ? attempt.node : selected;\n const attemptChooser = attempts.length > 1 && attempt ? (\n <div className=\"w-44 shrink-0\">\n <Select aria-label=\"Attempt\" value={String(attempt.id)} className=\"h-7 text-xs\" onChange={(event) => {\n const id = Number(event.target.value);\n if (runner.onAttemptChange) runner.onAttemptChange(id);\n else setAttemptSelection({ key, id });\n }} options={attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== undefined ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : \"\"}` }))} />\n </div>\n ) : null;\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(selected, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node: selected, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["sum","jsxs","jsx","TestFailureDetail","TestOutput","TestAttempts","JsonView","copy","UiCopy","UiCheck","UiWarningCircle","useTestRunner","useCopyFlash","useMemo","UiJson","buildNodeJson","SplitButton","buildNodeBrief","outcomeFor","OutcomeIcon","Icon","UiClass","ToolIcon","toolKeyFor","UiClock","formatTestDuration","Button","UiRefresh","UiStop","useState","_a","Select","cn","TabButton"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"names":[],"mappings":"AAqKA;;;;GAIG;AACH,wBAAgB,eAAe,gCAoE9B"}
1
+ {"version":3,"file":"TestDetailPanel.d.ts","sourceRoot":"","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"names":[],"mappings":"AAwKA;;;;GAIG;AACH,wBAAgB,eAAe,gCAoF9B"}
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState, useMemo } from "react";
3
3
  import { Icon } from "../Icon.js";
4
4
  import { Button } from "../../components/button.js";
5
+ import { Select } from "../../components/select.js";
5
6
  import { SplitButton } from "../../components/SplitButton.js";
6
7
  import { useCopyFlash } from "../../components/clipboard.js";
7
8
  import { cn } from "../../lib/utils.js";
@@ -9,7 +10,8 @@ import { JsonView } from "../JsonView.js";
9
10
  import { TabButton } from "../TabButton.js";
10
11
  import { useTestRunner } from "./context.js";
11
12
  import { buildNodeJson, buildNodeBrief } from "./copy.js";
12
- import { sum, outcomeFor, formatTestDuration } from "./status.js";
13
+ import { formatTestDuration, sum, outcomeFor } from "./status.js";
14
+ import { toolKeyFor } from "./marks.js";
13
15
  import { OutcomeIcon, ToolIcon } from "./MarkIcons.js";
14
16
  import { TestFailureDetail, TestOutput, TestAttempts } from "./TestFailureDetail.js";
15
17
  import { UiClass } from "../../icons/components/UiClass.js";
@@ -85,11 +87,11 @@ function CopyDetailsButton({ node }) {
85
87
  }
86
88
  );
87
89
  }
88
- function DetailHeader({ node, actions }) {
90
+ function DetailHeader({ node, actionNode, actions, attemptChooser }) {
89
91
  const runner = useTestRunner();
90
92
  const outcome = outcomeFor(node);
91
- const canRerun = !!runner.onRerun && node.framework !== "task";
92
- const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;
93
+ const canRerun = !!runner.onRerun && actionNode.framework !== "task";
94
+ const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;
93
95
  const location = node.file ? `${node.file}${node.line ? `:${node.line}` : ""}` : void 0;
94
96
  return /* @__PURE__ */ jsxs(
95
97
  "div",
@@ -100,7 +102,7 @@ function DetailHeader({ node, actions }) {
100
102
  outcome ? /* @__PURE__ */ jsx(OutcomeIcon, { outcome, className: "text-lg" }) : /* @__PURE__ */ jsx(Icon, { icon: UiClass, className: "shrink-0 text-lg text-muted-foreground" }),
101
103
  /* @__PURE__ */ jsx("h2", { className: "min-w-0 shrink truncate text-base font-semibold", title: node.name, children: node.name }),
102
104
  node.framework && /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground", children: [
103
- /* @__PURE__ */ jsx(ToolIcon, { tool: node.framework, className: "text-sm" }),
105
+ /* @__PURE__ */ jsx(ToolIcon, { tool: toolKeyFor(node), className: "text-sm" }),
104
106
  node.framework
105
107
  ] }),
106
108
  node.duration ? /* @__PURE__ */ jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground", children: [
@@ -116,6 +118,7 @@ function DetailHeader({ node, actions }) {
116
118
  }
117
119
  ),
118
120
  /* @__PURE__ */ jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [
121
+ attemptChooser,
119
122
  actions,
120
123
  /* @__PURE__ */ jsx(CopyDetailsButton, { node }),
121
124
  canRerun && /* @__PURE__ */ jsxs(
@@ -123,7 +126,7 @@ function DetailHeader({ node, actions }) {
123
126
  {
124
127
  size: "sm",
125
128
  disabled: runner.busy.rerun,
126
- onClick: () => runner.onRerun(node),
129
+ onClick: () => runner.onRerun(actionNode),
127
130
  title: "Rerun this test or subtree",
128
131
  children: [
129
132
  /* @__PURE__ */ jsx(Icon, { icon: UiRefresh, className: "mr-1 text-sm" }),
@@ -137,7 +140,7 @@ function DetailHeader({ node, actions }) {
137
140
  size: "sm",
138
141
  variant: "destructive",
139
142
  disabled: runner.busy.stop,
140
- onClick: () => runner.onStop(node),
143
+ onClick: () => runner.onStop(actionNode),
141
144
  title: "Stop this running task",
142
145
  children: [
143
146
  /* @__PURE__ */ jsx(Icon, { icon: UiStop, className: "mr-1 text-sm" }),
@@ -153,7 +156,8 @@ function DetailHeader({ node, actions }) {
153
156
  function TestDetailPanel() {
154
157
  var _a;
155
158
  const runner = useTestRunner();
156
- const node = runner.selected;
159
+ const selected = runner.selected;
160
+ const [attemptSelection, setAttemptSelection] = useState(null);
157
161
  const controlled = runner.activeTab !== void 0;
158
162
  const [internalTab, setInternalTab] = useState("detail");
159
163
  const activeTab = controlled ? runner.activeTab : internalTab;
@@ -162,22 +166,32 @@ function TestDetailPanel() {
162
166
  if (controlled) (_a2 = runner.onTabChange) == null ? void 0 : _a2.call(runner, id);
163
167
  else setInternalTab(id);
164
168
  };
165
- if (!node) {
169
+ if (!selected) {
166
170
  return /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground", children: /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
167
171
  /* @__PURE__ */ jsx(Icon, { icon: UiClass, className: "mb-2 block text-4xl" }),
168
172
  "Select a test to view its details."
169
173
  ] }) });
170
174
  }
175
+ const attempts = runner.adapters.resolveAttempts(selected, runner);
176
+ const key = selected.task_id ?? selected.name;
177
+ const attemptId = runner.activeAttempt ?? ((attemptSelection == null ? void 0 : attemptSelection.key) === key ? attemptSelection.id : void 0);
178
+ const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);
179
+ const node = attempts.length > 1 && attempt ? attempt.node : selected;
180
+ const attemptChooser = attempts.length > 1 && attempt ? /* @__PURE__ */ jsx("div", { className: "w-44 shrink-0", children: /* @__PURE__ */ jsx(Select, { "aria-label": "Attempt", value: String(attempt.id), className: "h-7 text-xs", onChange: (event) => {
181
+ const id = Number(event.target.value);
182
+ if (runner.onAttemptChange) runner.onAttemptChange(id);
183
+ else setAttemptSelection({ key, id });
184
+ }, options: attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== void 0 ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : ""}` })) }) }) : null;
171
185
  const detailAdapter = runner.adapters.resolveDetail(node, runner);
172
- const actionAdapter = runner.adapters.resolveActions(node, runner);
186
+ const actionAdapter = runner.adapters.resolveActions(selected, runner);
173
187
  const tabs = runner.adapters.resolveTabs(node, runner);
174
188
  const ownsScroll = runner.adapters.ownsScroll(node, runner);
175
189
  const body = (detailAdapter == null ? void 0 : detailAdapter.renderDetail) ? detailAdapter.renderDetail({ node, runner }) : /* @__PURE__ */ jsx(DefaultDetailBody, { node });
176
- const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node, runner });
190
+ const actions = (_a = actionAdapter == null ? void 0 : actionAdapter.nodeActions) == null ? void 0 : _a.call(actionAdapter, { node: selected, runner });
177
191
  const detailRegion = cn("min-h-0 flex-1 [container-type:size]", !ownsScroll && "overflow-y-auto");
178
192
  if (tabs.length === 0) {
179
193
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
180
- /* @__PURE__ */ jsx(DetailHeader, { node, actions }),
194
+ /* @__PURE__ */ jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
181
195
  /* @__PURE__ */ jsx("div", { className: detailRegion, children: body })
182
196
  ] });
183
197
  }
@@ -185,7 +199,7 @@ function TestDetailPanel() {
185
199
  const allTabs = [detailTab, ...tabs];
186
200
  const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;
187
201
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [
188
- /* @__PURE__ */ jsx(DetailHeader, { node, actions }),
202
+ /* @__PURE__ */ jsx(DetailHeader, { node, actionNode: selected, actions, attemptChooser }),
189
203
  /* @__PURE__ */ jsx("div", { className: "flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1", children: allTabs.map((t) => /* @__PURE__ */ jsx(
190
204
  TabButton,
191
205
  {
@@ -1 +1 @@
1
- {"version":3,"file":"TestDetailPanel.js","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actions }: { node: Test; actions?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && node.framework !== \"task\";\n const canStop = !!runner.onStop && !!node.can_stop && !!node.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={node.framework} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(node)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(node)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const node = runner.selected;\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!node) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(node, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actions={actions} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAS,kBAAkB,EAAE,QAAwB;;AACnD,QAAM,iBAAe,UAAK,aAAL,mBAAe,WAAU,KAAK;AACnD,QAAM,SAAS,cAAc,IAAI,IAAI,IAAI;AACzC,SACE,qBAAC,OAAA,EAAI,WAAU,yBACZ,UAAA;AAAA,IAAA,KAAK,WAAW,CAAC,KAAK,sCACpB,KAAA,EAAE,WAAU,2CAA2C,UAAA,KAAK,QAAA,CAAQ;AAAA,IAEtE,KAAK,kBAAkB,oBAAC,mBAAA,EAAkB,SAAS,KAAK,gBAAgB;AAAA,IACzE,oBAAC,cAAW,MAAY;AAAA,IACvB,KAAK,YAAY,KAAK,SAAS,SAAS,KAAK,oBAAC,cAAA,EAAa,UAAU,KAAK,SAAA,CAAU;AAAA,IACpF,UAAU,OAAO,QAAQ,KACxB,qBAAC,OAAA,EAAI,WAAU,sDACb,UAAA;AAAA,MAAA,qBAAC,QAAA,EAAM,UAAA;AAAA,QAAA,OAAO;AAAA,QAAM;AAAA,MAAA,GAAM;AAAA,MACzB,OAAO,SAAS,KAAK,qBAAC,QAAA,EAAM,UAAA;AAAA,QAAA,OAAO;AAAA,QAAO;AAAA,MAAA,GAAO;AAAA,MACjD,OAAO,SAAS,KAAK,qBAAC,QAAA,EAAM,UAAA;AAAA,QAAA,OAAO;AAAA,QAAO;AAAA,MAAA,GAAO;AAAA,MACjD,OAAO,UAAU,KAAK,qBAAC,QAAA,EAAM,UAAA;AAAA,QAAA,OAAO;AAAA,QAAQ;AAAA,MAAA,EAAA,CAAQ;AAAA,IAAA,GACvD;AAAA,IAED,KAAK,UAAU,QACd,qBAAC,OAAA,EAAI,WAAU,aACb,UAAA;AAAA,MAAA,oBAAC,OAAA,EAAI,WAAU,6DAA4D,UAAA,UAAM;AAAA,MACjF,oBAAC,UAAA,EAAS,MAAM,KAAK,OAAA,CAAQ;AAAA,IAAA,EAAA,CAC/B;AAAA,EAAA,GAEJ;AAEJ;AAEA,SAAS,aAAa,MAAgB,MAAgD;AACpF,QAAM,OAAO,KAAK,OAAO,EAAE,MAAM,KAAK,KAAA,IAAS,CAAA;AAC/C,MAAI,YAAY,MAAM;AACpB,WAAO,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,UAAU,MAAM,OAAO,KAAK,QAAQ,UAAU,MAAM;AAAA,IAAC,EAAA;AAAA,EAC5F;AACA,SAAO,EAAE,OAAO,KAAK,OAAO,GAAG,MAAM,UAAU,MAAM,KAAK,KAAK,KAAK,EAAA;AACtE;AAEA,MAAM,aAAa,EAAE,MAAM,gBAAgB,QAAQ,UAAU,OAAO,cAAA;AACpE,MAAM,YAAY,EAAE,MAAM,QAAQ,QAAQ,SAAS,OAAO,gBAAA;AAQ1D,SAAS,kBAAkB,EAAE,QAAwB;AACnD,QAAM,SAAS,cAAA;AACf,QAAM,EAAE,OAAO,KAAA,IAAS,aAAA;AACxB,QAAM,WAAW,QAAQ,MAAM,OAAO,SAAS,YAAY,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC;AACxF,QAAM,QAA4B;AAAA,IAChC,EAAE,OAAO,gBAAgB,MAAM,QAAQ,UAAU,MAAM,KAAK,cAAc,IAAI,CAAC,EAAA;AAAA,IAC/E,GAAG,SAAS,MAAM,IAAI,CAAC,SAAS,aAAa,MAAM,IAAI,CAAC;AAAA,EAAA;AAE1D,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,WAAW,KAAK;AAAA,MACvB,MAAM,UAAU,KAAK;AAAA,MACrB,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,OAAM;AAAA,MACN,OAAM;AAAA,MACN,SAAS,MAAM,KAAK,eAAe,MAAM,QAAQ,CAAC;AAAA,MAClD;AAAA,IAAA;AAAA,EAAA;AAGN;AAEA,SAAS,aAAa,EAAE,MAAM,WAAgD;AAC5E,QAAM,SAAS,cAAA;AACf,QAAM,UAAU,WAAW,IAAI;AAC/B,QAAM,WAAW,CAAC,CAAC,OAAO,WAAW,KAAK,cAAc;AACxD,QAAM,UAAU,CAAC,CAAC,OAAO,UAAU,CAAC,CAAC,KAAK,YAAY,CAAC,CAAC,KAAK;AAC7D,QAAM,WAAW,KAAK,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,OAAO,IAAI,KAAK,IAAI,KAAK,EAAE,KAAK;AACjF,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAU;AAAA,MACV,sBAAmB;AAAA,MAElB,UAAA;AAAA,QAAA,UACC,oBAAC,aAAA,EAAY,SAAkB,WAAU,UAAA,CAAU,IAEnD,oBAAC,MAAA,EAAK,MAAM,SAAS,WAAU,yCAAA,CAAyC;AAAA,QAE1E,oBAAC,QAAG,WAAU,mDAAkD,OAAO,KAAK,MACzE,eAAK,KAAA,CACR;AAAA,QACC,KAAK,aACJ,qBAAC,QAAA,EAAK,WAAU,wGACd,UAAA;AAAA,UAAA,oBAAC,UAAA,EAAS,MAAM,KAAK,WAAW,WAAU,WAAU;AAAA,UACnD,KAAK;AAAA,QAAA,GACR;AAAA,QAED,KAAK,WACJ,qBAAC,QAAA,EAAK,WAAU,yEACd,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAK,MAAM,SAAS,WAAU,WAAU;AAAA,UACxC,mBAAmB,KAAK,QAAQ;AAAA,QAAA,EAAA,CACnC,IACE;AAAA,QACH,YACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO;AAAA,YAEN,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAGL,qBAAC,OAAA,EAAI,WAAU,4CACZ,UAAA;AAAA,UAAA;AAAA,UACD,oBAAC,qBAAkB,MAAY;AAAA,UAC9B,YACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU,OAAO,KAAK;AAAA,cACtB,SAAS,MAAM,OAAO,QAAS,IAAI;AAAA,cACnC,OAAM;AAAA,cAEN,UAAA;AAAA,gBAAA,oBAAC,MAAA,EAAK,MAAM,WAAW,WAAU,gBAAe;AAAA,gBAC/C,OAAO,KAAK,QAAQ,eAAe;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAGvC,WACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,UAAU,OAAO,KAAK;AAAA,cACtB,SAAS,MAAM,OAAO,OAAQ,IAAI;AAAA,cAClC,OAAM;AAAA,cAEN,UAAA;AAAA,gBAAA,oBAAC,MAAA,EAAK,MAAM,QAAQ,WAAU,gBAAe;AAAA,gBAC5C,OAAO,KAAK,OAAO,gBAAgB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACtC,EAAA,CAEJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;AAOO,SAAS,kBAAkB;;AAChC,QAAM,SAAS,cAAA;AACf,QAAM,OAAO,OAAO;AAIpB,QAAM,aAAa,OAAO,cAAc;AACxC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAiB,QAAQ;AAC/D,QAAM,YAAY,aAAa,OAAO,YAAa;AACnD,QAAM,eAAe,CAAC,OAAe;;AACnC,QAAI,WAAY,EAAAA,MAAA,OAAO,gBAAP,gBAAAA,IAAA,aAAqB;AAAA,wBACjB,EAAE;AAAA,EACxB;AAEA,MAAI,CAAC,MAAM;AACT,+BACG,OAAA,EAAI,WAAU,yEACb,UAAA,qBAAC,OAAA,EAAI,WAAU,eACb,UAAA;AAAA,MAAA,oBAAC,MAAA,EAAK,MAAM,SAAS,WAAU,uBAAsB;AAAA,MAAE;AAAA,IAAA,EAAA,CAEzD,EAAA,CACF;AAAA,EAEJ;AAEA,QAAM,gBAAgB,OAAO,SAAS,cAAc,MAAM,MAAM;AAChE,QAAM,gBAAgB,OAAO,SAAS,eAAe,MAAM,MAAM;AACjE,QAAM,OAAO,OAAO,SAAS,YAAY,MAAM,MAAM;AACrD,QAAM,aAAa,OAAO,SAAS,WAAW,MAAM,MAAM;AAE1D,QAAM,QAAO,+CAAe,gBACxB,cAAc,aAAa,EAAE,MAAM,OAAA,CAAQ,IAC3C,oBAAC,mBAAA,EAAkB,KAAA,CAAY;AACnC,QAAM,WAAU,oDAAe,gBAAf,uCAA6B,EAAE,MAAM;AAGrD,QAAM,eAAe,GAAG,wCAAwC,CAAC,cAAc,iBAAiB;AAGhG,MAAI,KAAK,WAAW,GAAG;AACrB,WACE,qBAAC,OAAA,EAAI,WAAU,gCACb,UAAA;AAAA,MAAA,oBAAC,cAAA,EAAa,MAAY,QAAA,CAAkB;AAAA,MAC5C,oBAAC,OAAA,EAAI,WAAW,cAAe,UAAA,KAAA,CAAK;AAAA,IAAA,GACtC;AAAA,EAEJ;AAEA,QAAM,YAAY,EAAE,IAAI,UAAU,OAAO,UAAU,QAAQ,MAAM,KAAA;AACjE,QAAM,UAAU,CAAC,WAAW,GAAG,IAAI;AACnC,QAAM,SAAS,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,SAAS,KAAK;AAE1D,SACE,qBAAC,OAAA,EAAI,WAAU,gCACb,UAAA;AAAA,IAAA,oBAAC,cAAA,EAAa,MAAY,QAAA,CAAkB;AAAA,wBAC3C,OAAA,EAAI,WAAU,qFACZ,UAAA,QAAQ,IAAI,CAAC,MACZ;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,QAAQ,OAAO,OAAO,EAAE;AAAA,QACxB,SAAS,MAAM,aAAa,EAAE,EAAE;AAAA,QAChC,OAAO,EAAE;AAAA,MAAA;AAAA,MAHJ,EAAE;AAAA,IAAA,CAKV,GACH;AAAA,wBACC,OAAA,EAAI,WAAW,cAAe,UAAA,OAAO,SAAO,CAAE;AAAA,EAAA,GACjD;AAEJ;"}
1
+ {"version":3,"file":"TestDetailPanel.js","sources":["../../../src/data/test-runner/TestDetailPanel.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from \"react\";\nimport { Icon } from \"../Icon\";\nimport { Button } from \"../../components/button\";\nimport { Select } from \"../../components/select\";\nimport { SplitButton } from \"../../components/SplitButton\";\nimport { useCopyFlash } from \"../../components/clipboard\";\nimport type { DropdownMenuItem } from \"../../overlay/DropdownMenu\";\nimport { cn } from \"../../lib/utils\";\nimport { JsonView } from \"../JsonView\";\nimport { TabButton } from \"../TabButton\";\nimport {\n UiCheck,\n UiClass,\n UiClock,\n UiCopy,\n UiJson,\n UiRefresh,\n UiStop,\n UiWarningCircle,\n} from \"../../icons\";\nimport type { Test } from \"./types\";\nimport { useTestRunner } from \"./context\";\nimport { buildNodeBrief, buildNodeJson, type CopyItem } from \"./copy\";\nimport { formatTestDuration, outcomeFor, sum } from \"./status\";\nimport { toolKeyFor } from \"./marks\";\nimport { OutcomeIcon, ToolIcon } from \"./MarkIcons\";\nimport { TestAttempts, TestFailureDetail, TestOutput } from \"./TestFailureDetail\";\n\n/** Built-in detail body — used when no adapter contributes `renderDetail`. */\nfunction DefaultDetailBody({ node }: { node: Test }) {\n const hasChildren = (node.children?.length ?? 0) > 0;\n const counts = hasChildren ? sum(node) : null;\n return (\n <div className=\"space-y-4 p-density-4\">\n {node.message && !node.failure_detail && (\n <p className=\"whitespace-pre-wrap break-words text-sm\">{node.message}</p>\n )}\n {node.failure_detail && <TestFailureDetail failure={node.failure_detail} />}\n <TestOutput node={node} />\n {node.attempts && node.attempts.length > 0 && <TestAttempts attempts={node.attempts} />}\n {counts && counts.total > 0 && (\n <div className=\"flex flex-wrap gap-3 text-xs text-muted-foreground\">\n <span>{counts.total} total</span>\n {counts.passed > 0 && <span>{counts.passed} passed</span>}\n {counts.failed > 0 && <span>{counts.failed} failed</span>}\n {counts.skipped > 0 && <span>{counts.skipped} skipped</span>}\n </div>\n )}\n {node.detail != null && (\n <div className=\"space-y-1\">\n <div className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Detail</div>\n <JsonView data={node.detail} />\n </div>\n )}\n </div>\n );\n}\n\nfunction copyMenuItem(item: CopyItem, copy: (text: string) => void): DropdownMenuItem {\n const icon = item.icon ? { icon: item.icon } : {};\n if (\"reason\" in item) {\n return { label: item.label, ...icon, disabled: true, title: item.reason, onSelect: () => {} };\n }\n return { label: item.label, ...icon, onSelect: () => copy(item.value) };\n}\n\nconst COPY_LABEL = { idle: \"Copy details\", copied: \"Copied\", error: \"Copy failed\" } as const;\nconst COPY_ICON = { idle: UiCopy, copied: UiCheck, error: UiWarningCircle } as const;\n\n/**\n * \"Copy details\" split button: the primary action copies a Markdown brief of\n * the generic sections plus every matching adapter's `copy` sections; the menu\n * offers the node as JSON and the adapters' items, disabled with a reason when\n * they have nothing to copy. The label flips to \"Copied\" or \"Copy failed\".\n */\nfunction CopyDetailsButton({ node }: { node: Test }) {\n const runner = useTestRunner();\n const { state, copy } = useCopyFlash();\n const sections = useMemo(() => runner.adapters.resolveCopy(node, runner), [node, runner]);\n const items: DropdownMenuItem[] = [\n { label: \"Copy as JSON\", icon: UiJson, onSelect: () => copy(buildNodeJson(node)) },\n ...sections.items.map((item) => copyMenuItem(item, copy)),\n ];\n return (\n <SplitButton\n label={COPY_LABEL[state]}\n icon={COPY_ICON[state]}\n variant=\"outline\"\n size=\"sm\"\n align=\"right\"\n title=\"More copy options\"\n onClick={() => copy(buildNodeBrief(node, sections))}\n items={items}\n />\n );\n}\n\nfunction DetailHeader({ node, actionNode, actions, attemptChooser }: { node: Test; actionNode: Test; actions?: ReactNode; attemptChooser?: ReactNode }) {\n const runner = useTestRunner();\n const outcome = outcomeFor(node);\n const canRerun = !!runner.onRerun && actionNode.framework !== \"task\";\n const canStop = !!runner.onStop && !!actionNode.can_stop && !!actionNode.task_id;\n const location = node.file ? `${node.file}${node.line ? `:${node.line}` : \"\"}` : undefined;\n return (\n <div\n className=\"flex min-w-0 items-center gap-2 border-b border-border px-density-4 py-density-2\"\n data-detail-header=\"\"\n >\n {outcome ? (\n <OutcomeIcon outcome={outcome} className=\"text-lg\" />\n ) : (\n <Icon icon={UiClass} className=\"shrink-0 text-lg text-muted-foreground\" />\n )}\n <h2 className=\"min-w-0 shrink truncate text-base font-semibold\" title={node.name}>\n {node.name}\n </h2>\n {node.framework && (\n <span className=\"inline-flex shrink-0 items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n <ToolIcon tool={toolKeyFor(node)} className=\"text-sm\" />\n {node.framework}\n </span>\n )}\n {node.duration ? (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <Icon icon={UiClock} className=\"text-xs\" />\n {formatTestDuration(node.duration)}\n </span>\n ) : null}\n {location && (\n <span\n className=\"min-w-0 shrink-[100] truncate font-mono text-xs text-muted-foreground\"\n title={location}\n >\n {location}\n </span>\n )}\n <div className=\"ml-auto flex shrink-0 items-center gap-1\">\n {attemptChooser}\n {actions}\n <CopyDetailsButton node={node} />\n {canRerun && (\n <Button\n size=\"sm\"\n disabled={runner.busy.rerun}\n onClick={() => runner.onRerun!(actionNode)}\n title=\"Rerun this test or subtree\"\n >\n <Icon icon={UiRefresh} className=\"mr-1 text-sm\" />\n {runner.busy.rerun ? \"Running...\" : \"Rerun\"}\n </Button>\n )}\n {canStop && (\n <Button\n size=\"sm\"\n variant=\"destructive\"\n disabled={runner.busy.stop}\n onClick={() => runner.onStop!(actionNode)}\n title=\"Stop this running task\"\n >\n <Icon icon={UiStop} className=\"mr-1 text-sm\" />\n {runner.busy.stop ? \"Stopping...\" : \"Stop\"}\n </Button>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The right pane. Resolves the node's adapter for body / tabs / header actions,\n * falling back to the built-in default body. Shows an empty state when nothing\n * is selected.\n */\nexport function TestDetailPanel() {\n const runner = useTestRunner();\n const selected = runner.selected;\n const [attemptSelection, setAttemptSelection] = useState<{ key: string; id: number } | null>(null);\n // Tab state is controlled when the host supplies activeTab (e.g. to mirror it\n // into the URL); otherwise the panel owns it. The render below tolerates an\n // unknown id by falling back to the built-in \"detail\" tab.\n const controlled = runner.activeTab !== undefined;\n const [internalTab, setInternalTab] = useState<string>(\"detail\");\n const activeTab = controlled ? runner.activeTab! : internalTab;\n const setActiveTab = (id: string) => {\n if (controlled) runner.onTabChange?.(id);\n else setInternalTab(id);\n };\n\n if (!selected) {\n return (\n <div className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n <div className=\"text-center\">\n <Icon icon={UiClass} className=\"mb-2 block text-4xl\" />\n Select a test to view its details.\n </div>\n </div>\n );\n }\n\n const attempts = runner.adapters.resolveAttempts(selected, runner);\n const key = selected.task_id ?? selected.name;\n const attemptId = runner.activeAttempt ?? (attemptSelection?.key === key ? attemptSelection.id : undefined);\n const attempt = attempts.find((a) => a.id === attemptId) ?? attempts.at(-1);\n const node = attempts.length > 1 && attempt ? attempt.node : selected;\n const attemptChooser = attempts.length > 1 && attempt ? (\n <div className=\"w-44 shrink-0\">\n <Select aria-label=\"Attempt\" value={String(attempt.id)} className=\"h-7 text-xs\" onChange={(event) => {\n const id = Number(event.target.value);\n if (runner.onAttemptChange) runner.onAttemptChange(id);\n else setAttemptSelection({ key, id });\n }} options={attempts.map((a) => ({ value: String(a.id), label: `Attempt ${a.id}/${attempts.length} · ${a.status}${a.durationMs !== undefined ? ` · ${formatTestDuration(a.durationMs * 1e6)}` : \"\"}` }))} />\n </div>\n ) : null;\n\n const detailAdapter = runner.adapters.resolveDetail(node, runner);\n const actionAdapter = runner.adapters.resolveActions(selected, runner);\n const tabs = runner.adapters.resolveTabs(node, runner);\n const ownsScroll = runner.adapters.ownsScroll(node, runner);\n\n const body = detailAdapter?.renderDetail\n ? detailAdapter.renderDetail({ node, runner })\n : <DefaultDetailBody node={node} />;\n const actions = actionAdapter?.nodeActions?.({ node: selected, runner });\n\n // A size container, so log blocks can cap themselves at a share of the pane (cqh).\n const detailRegion = cn(\"min-h-0 flex-1 [container-type:size]\", !ownsScroll && \"overflow-y-auto\");\n\n // No extra tabs → just the body under the header.\n if (tabs.length === 0) {\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className={detailRegion}>{body}</div>\n </div>\n );\n }\n\n const detailTab = { id: \"detail\", label: \"Detail\", render: () => body };\n const allTabs = [detailTab, ...tabs];\n const active = allTabs.find((t) => t.id === activeTab) ?? detailTab;\n\n return (\n <div className=\"flex h-full min-h-0 flex-col\">\n <DetailHeader node={node} actionNode={selected} actions={actions} attemptChooser={attemptChooser} />\n <div className=\"flex shrink-0 items-center gap-1 border-b border-border px-density-2 py-density-1\">\n {allTabs.map((t) => (\n <TabButton\n key={t.id}\n active={active.id === t.id}\n onClick={() => setActiveTab(t.id)}\n label={t.label}\n />\n ))}\n </div>\n <div className={detailRegion}>{active.render()}</div>\n </div>\n );\n}\n"],"names":["_a"],"mappings":"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"}
@@ -34,6 +34,8 @@ function TestRunner({
34
34
  busy,
35
35
  activeTab,
36
36
  onTabChange,
37
+ activeAttempt,
38
+ onAttemptChange,
37
39
  onSelect,
38
40
  onFiltersChange,
39
41
  onExpandAllChange,
@@ -63,6 +65,8 @@ function TestRunner({
63
65
  busy: busy ?? {},
64
66
  activeTab,
65
67
  onTabChange,
68
+ activeAttempt,
69
+ onAttemptChange,
66
70
  onSelect,
67
71
  onFiltersChange,
68
72
  onExpandAllChange,
@@ -82,6 +86,8 @@ function TestRunner({
82
86
  busy,
83
87
  activeTab,
84
88
  onTabChange,
89
+ activeAttempt,
90
+ onAttemptChange,
85
91
  onSelect,
86
92
  onFiltersChange,
87
93
  onExpandAllChange,