@tanstack/ai-devtools-core 0.3.38 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/Shell/DKCFIT6H.js +12142 -0
  2. package/dist/Shell/SLOX2TPU.js +12119 -0
  3. package/dist/Shell/Z6HLJOVZ.js +10170 -0
  4. package/dist/dev.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/production/Shell/DKCFIT6H.js +12142 -0
  7. package/dist/production/Shell/SLOX2TPU.js +12119 -0
  8. package/dist/production/Shell/Z6HLJOVZ.js +10170 -0
  9. package/dist/production/dev.js +1 -1
  10. package/dist/production/index.js +1 -1
  11. package/dist/production/server.js +1 -1
  12. package/dist/server.js +1 -1
  13. package/package.json +4 -4
  14. package/src/components/Shell.tsx +74 -30
  15. package/src/components/conversation/IterationCard.tsx +261 -12
  16. package/src/components/conversation/IterationTimeline.tsx +66 -16
  17. package/src/components/conversation/index.ts +0 -9
  18. package/src/components/hooks/FixtureNamePopover.tsx +69 -0
  19. package/src/components/hooks/GenerationPanel.tsx +847 -0
  20. package/src/components/hooks/HookDashboard.tsx +292 -0
  21. package/src/components/hooks/HookDetails.tsx +1757 -0
  22. package/src/components/hooks/ToolFixtureForm.tsx +347 -0
  23. package/src/components/hooks/hook-dashboard-model.ts +134 -0
  24. package/src/components/hooks/index.ts +4 -0
  25. package/src/components/hooks/preview-messages.ts +117 -0
  26. package/src/components/hooks/preview-model.ts +195 -0
  27. package/src/components/utils/format.ts +0 -58
  28. package/src/env.d.ts +5 -0
  29. package/src/store/ai-context.tsx +1165 -159
  30. package/src/store/ai-store.ts +0 -1
  31. package/src/store/hook-registry.ts +815 -0
  32. package/src/store/message-event-utils.ts +60 -0
  33. package/src/styles/use-styles.ts +1558 -99
  34. package/dist/Shell/MRWZEHAJ.js +0 -6225
  35. package/dist/Shell/RRRNZJ5E.js +0 -7560
  36. package/dist/production/Shell/MRWZEHAJ.js +0 -6225
  37. package/dist/production/Shell/RRRNZJ5E.js +0 -7560
  38. package/src/components/ConversationDetails.tsx +0 -216
  39. package/src/components/ConversationsList.tsx +0 -21
  40. package/src/components/conversation/ActivityEventsTab.tsx +0 -57
  41. package/src/components/conversation/ChunkBadges.tsx +0 -51
  42. package/src/components/conversation/ChunkItem.tsx +0 -273
  43. package/src/components/conversation/ChunksCollapsible.tsx +0 -56
  44. package/src/components/conversation/ChunksTab.tsx +0 -87
  45. package/src/components/conversation/ConversationHeader.tsx +0 -106
  46. package/src/components/conversation/ConversationTabs.tsx +0 -205
  47. package/src/components/conversation/MessageCard.tsx +0 -174
  48. package/src/components/conversation/MessageGroup.tsx +0 -101
  49. package/src/components/conversation/MessagesTab.tsx +0 -30
  50. package/src/components/conversation/SummariesTab.tsx +0 -91
  51. package/src/components/conversation/ToolCallDisplay.tsx +0 -99
  52. package/src/components/list/ConversationRow.tsx +0 -85
  53. package/src/components/list/index.ts +0 -1
@@ -2,7 +2,7 @@ import { constructCoreClass } from '@tanstack/devtools-utils/solid/class';
2
2
 
3
3
  // src/core.ts
4
4
  var [AiDevtoolsCore, AiDevtoolsCoreNoOp] = constructCoreClass(
5
- () => import('./Shell/RRRNZJ5E.js')
5
+ () => import('./Shell/DKCFIT6H.js')
6
6
  );
7
7
 
8
8
  export { AiDevtoolsCore };
@@ -2,7 +2,7 @@ import { constructCoreClass } from '@tanstack/devtools-utils/solid/class';
2
2
 
3
3
  // src/core.ts
4
4
  var [AiDevtoolsCore, AiDevtoolsCoreNoOp] = constructCoreClass(
5
- () => import('./Shell/RRRNZJ5E.js')
5
+ () => import('./Shell/SLOX2TPU.js')
6
6
  );
7
7
 
8
8
  export { AiDevtoolsCore };
@@ -2,7 +2,7 @@ import { constructCoreClass } from '@tanstack/devtools-utils/solid/class';
2
2
 
3
3
  // src/core.ts
4
4
  var [AiDevtoolsCore, AiDevtoolsCoreNoOp] = constructCoreClass(
5
- () => import('./Shell/MRWZEHAJ.js')
5
+ () => import('./Shell/Z6HLJOVZ.js')
6
6
  );
7
7
 
8
8
  export { AiDevtoolsCore };
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import { constructCoreClass } from '@tanstack/devtools-utils/solid/class';
2
2
 
3
3
  // src/core.ts
4
4
  var [AiDevtoolsCore, AiDevtoolsCoreNoOp] = constructCoreClass(
5
- () => import('./Shell/MRWZEHAJ.js')
5
+ () => import('./Shell/Z6HLJOVZ.js')
6
6
  );
7
7
 
8
8
  // src/index.ts
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@tanstack/ai-devtools-core",
3
- "version": "0.3.38",
3
+ "version": "0.4.1",
4
4
  "description": "Core TanStack AI Devtools plugin for inspecting chat messages, tool calls, streams, and errors.",
5
5
  "author": "",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/TanStack/ai.git",
10
- "directory": "packages/typescript/ai"
10
+ "directory": "packages/ai"
11
11
  },
12
12
  "type": "module",
13
13
  "types": "./dist/index.d.ts",
@@ -76,8 +76,8 @@
76
76
  "@tanstack/devtools-utils": "^0.4.0",
77
77
  "goober": "^2.1.18",
78
78
  "solid-js": "^1.9.10",
79
- "@tanstack/ai": "0.22.0",
80
- "@tanstack/ai-event-client": "0.3.11"
79
+ "@tanstack/ai": "0.23.0",
80
+ "@tanstack/ai-event-client": "0.4.1"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@vitest/coverage-v8": "4.0.14",
@@ -5,10 +5,14 @@ import {
5
5
  MainPanel,
6
6
  ThemeContextProvider,
7
7
  } from '@tanstack/devtools-ui'
8
+ import {
9
+ aiEventClient,
10
+ createAIDevtoolsEventEnvelope,
11
+ emitAIDevtoolsEvent,
12
+ } from '@tanstack/ai-event-client'
8
13
  import { useStyles } from '../styles/use-styles'
9
14
  import { AIProvider } from '../store/ai-context'
10
- import { ConversationsList } from './ConversationsList'
11
- import { ConversationDetails } from './ConversationDetails'
15
+ import { HookDashboard, HookDetails } from './hooks'
12
16
 
13
17
  import type { TanStackDevtoolsTheme } from '@tanstack/devtools-ui'
14
18
 
@@ -62,43 +66,83 @@ function DevtoolsContent() {
62
66
  onMount(() => {
63
67
  document.addEventListener('mousemove', handleMouseMove)
64
68
  document.addEventListener('mouseup', handleMouseUp)
69
+
70
+ const openedAt = Date.now()
71
+ emitAIDevtoolsEvent('devtools:opened', {
72
+ ...createAIDevtoolsEventEnvelope({
73
+ eventType: 'devtools:opened',
74
+ source: 'devtools',
75
+ visibility: 'devtools-action',
76
+ timestamp: openedAt,
77
+ }),
78
+ })
79
+ emitAIDevtoolsEvent('devtools:request-state', {
80
+ ...createAIDevtoolsEventEnvelope({
81
+ eventType: 'devtools:request-state',
82
+ source: 'devtools',
83
+ visibility: 'devtools-action',
84
+ timestamp: openedAt + 1,
85
+ }),
86
+ })
65
87
  })
66
88
 
67
89
  onCleanup(() => {
68
90
  document.removeEventListener('mousemove', handleMouseMove)
69
91
  document.removeEventListener('mouseup', handleMouseUp)
92
+ // If the panel unmounts mid-drag, the mouseup handler never fires;
93
+ // reset the global drag styles so the host page isn't stuck with
94
+ // col-resize cursor / unselectable body.
95
+ if (isDragging()) {
96
+ setIsDragging(false)
97
+ document.body.style.cursor = ''
98
+ document.body.style.userSelect = ''
99
+ }
100
+ aiEventClient.emit('devtools:closed', {
101
+ ...createAIDevtoolsEventEnvelope({
102
+ eventType: 'devtools:closed',
103
+ source: 'devtools',
104
+ visibility: 'devtools-action',
105
+ timestamp: Date.now(),
106
+ }),
107
+ })
70
108
  })
71
109
 
72
110
  return (
73
111
  <MainPanel>
74
- <Header>
75
- <HeaderLogo flavor={{ light: '#ec4899', dark: '#ec4899' }}>
76
- TanStack AI
77
- </HeaderLogo>
78
- </Header>
79
-
80
- <div class={styles().mainContainer}>
81
- <div
82
- class={styles().leftPanel}
83
- style={{
84
- width: `${leftPanelWidth()}px`,
85
- 'min-width': '150px',
86
- 'max-width': '800px',
87
- }}
88
- >
89
- {/* Section header */}
90
- <div class={styles().shell.sectionHeader}>Active Conversations</div>
91
-
92
- <ConversationsList />
93
- </div>
94
-
95
- <div
96
- class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
97
- onMouseDown={handleMouseDown}
98
- />
99
-
100
- <div class={styles().rightPanel} style={{ flex: 1 }}>
101
- <ConversationDetails />
112
+ <div class={styles().shellRoot} data-testid="ai-devtools-panel">
113
+ <Header>
114
+ <HeaderLogo flavor={{ light: '#ec4899', dark: '#ec4899' }}>
115
+ TanStack AI
116
+ </HeaderLogo>
117
+ </Header>
118
+
119
+ <div class={styles().mainContainer}>
120
+ <div
121
+ class={styles().leftPanel}
122
+ data-testid="ai-devtools-left-panel"
123
+ style={{
124
+ width: `${leftPanelWidth()}px`,
125
+ 'min-width': '150px',
126
+ 'max-width': '800px',
127
+ }}
128
+ >
129
+ <div class={styles().shell.sectionHeader}>AI Hooks</div>
130
+
131
+ <HookDashboard />
132
+ </div>
133
+
134
+ <div
135
+ class={`${styles().dragHandle} ${isDragging() ? 'dragging' : ''}`}
136
+ onMouseDown={handleMouseDown}
137
+ />
138
+
139
+ <div
140
+ class={styles().rightPanel}
141
+ data-testid="ai-devtools-right-panel"
142
+ style={{ flex: 1 }}
143
+ >
144
+ <HookDetails />
145
+ </div>
102
146
  </div>
103
147
  </div>
104
148
  </MainPanel>
@@ -9,8 +9,18 @@ import {
9
9
  } from 'solid-js'
10
10
  import { JsonTree } from '@tanstack/devtools-ui'
11
11
  import { useStyles } from '../../styles/use-styles'
12
+ import {
13
+ createHoverTarget,
14
+ getHoverDataAttributes,
15
+ isMessageHighlighted,
16
+ structuredOutputJsonItems,
17
+ structuredOutputPartId,
18
+ toolCallPartId,
19
+ toolResultPartId,
20
+ } from '../hooks/preview-model'
12
21
  import { formatDuration } from '../utils'
13
22
  import { SystemPromptItem } from './IterationTimeline'
23
+ import type { HoverTarget } from '../hooks/preview-model'
14
24
  import type {
15
25
  Iteration,
16
26
  Message,
@@ -25,6 +35,12 @@ interface IterationCardProps {
25
35
  messages: Array<Message>
26
36
  index: number
27
37
  isLast: boolean
38
+ hoverTarget?: HoverTarget | null
39
+ onHoverTarget?: (target: HoverTarget | null) => void
40
+ }
41
+
42
+ type StructuredOutputMessagePart = NonNullable<Message['parts']>[number] & {
43
+ type: 'structured-output'
28
44
  }
29
45
 
30
46
  // --- Step types ---
@@ -32,12 +48,37 @@ interface IterationCardProps {
32
48
  type IterationStep =
33
49
  | { kind: 'middleware'; event: MiddlewareEvent }
34
50
  | { kind: 'thinking'; message: Message }
51
+ | {
52
+ kind: 'structured_output'
53
+ message: Message
54
+ part: StructuredOutputMessagePart
55
+ }
35
56
  | { kind: 'assistant'; message: Message }
36
57
  | { kind: 'tool_call'; toolCall: ToolCall; message: Message }
37
58
  | { kind: 'tool_result'; message: Message }
38
59
 
39
60
  // --- Helpers ---
40
61
 
62
+ function getApprovalStatus(toolCall: ToolCall): string | undefined {
63
+ if (toolCall.approvalApproved === true || toolCall.state === 'approved') {
64
+ return 'approved'
65
+ }
66
+ if (toolCall.approvalApproved === false || toolCall.state === 'denied') {
67
+ return 'denied'
68
+ }
69
+ if (
70
+ toolCall.approvalRequired ||
71
+ toolCall.approvalId ||
72
+ toolCall.state === 'approval-requested'
73
+ ) {
74
+ return 'approval requested'
75
+ }
76
+ if (toolCall.state === 'approval-responded') {
77
+ return 'responded'
78
+ }
79
+ return undefined
80
+ }
81
+
41
82
  function getIterationLabel(iter: Iteration, displayIndex: number): string {
42
83
  if (!iter.completedAt) return `Iteration ${displayIndex} — Generating...`
43
84
  if (iter.finishReason === 'error') return `Iteration ${displayIndex} — Error`
@@ -70,6 +111,15 @@ function buildSteps(
70
111
  if (msg.thinkingContent) {
71
112
  steps.push({ kind: 'thinking', message: msg })
72
113
  }
114
+ for (const part of msg.parts ?? []) {
115
+ if (part.type === 'structured-output') {
116
+ steps.push({
117
+ kind: 'structured_output',
118
+ message: msg,
119
+ part: part as StructuredOutputMessagePart,
120
+ })
121
+ }
122
+ }
73
123
  if (msg.toolCalls && msg.toolCalls.length > 0) {
74
124
  for (const tc of msg.toolCalls) {
75
125
  steps.push({ kind: 'tool_call', toolCall: tc, message: msg })
@@ -157,7 +207,7 @@ const MiddlewareStep: Component<{
157
207
  <Show when={expanded() && hasChanges()}>
158
208
  <div class={s().mwChangesContainer}>
159
209
  <JsonTree
160
- value={ev().configChanges as Record<string, unknown>}
210
+ value={ev().configChanges}
161
211
  defaultExpansionDepth={2}
162
212
  copyable
163
213
  />
@@ -169,6 +219,7 @@ const MiddlewareStep: Component<{
169
219
 
170
220
  const ThinkingStep: Component<{
171
221
  step: Extract<IterationStep, { kind: 'thinking' }>
222
+ onHoverTarget?: (target: HoverTarget | null) => void
172
223
  }> = (props) => {
173
224
  const styles = useStyles()
174
225
  const s = () => styles().iterationTimeline
@@ -180,7 +231,19 @@ const ThinkingStep: Component<{
180
231
 
181
232
  return (
182
233
  <>
183
- <div class={s().step}>
234
+ <div
235
+ {...getHoverDataAttributes({ messageIds: [msg().id] })}
236
+ class={s().step}
237
+ onMouseEnter={() =>
238
+ props.onHoverTarget?.(
239
+ createHoverTarget({
240
+ messageIds: [msg().id],
241
+ origin: 'timeline',
242
+ }),
243
+ )
244
+ }
245
+ onMouseLeave={() => props.onHoverTarget?.(null)}
246
+ >
184
247
  <span class={`${s().stepPrefix} ${s().stepPrefixThinking}`}>
185
248
  Thinking
186
249
  </span>
@@ -201,8 +264,78 @@ const ThinkingStep: Component<{
201
264
  )
202
265
  }
203
266
 
267
+ const StructuredOutputStep: Component<{
268
+ step: Extract<IterationStep, { kind: 'structured_output' }>
269
+ onHoverTarget?: (target: HoverTarget | null) => void
270
+ }> = (props) => {
271
+ const styles = useStyles()
272
+ const s = () => styles().iterationTimeline
273
+ const [expanded, setExpanded] = createSignal(true)
274
+ const msg = () => props.step.message
275
+ const part = () => props.step.part
276
+ const partId = () => structuredOutputPartId(msg().id)
277
+ const jsonItems = createMemo(() => structuredOutputJsonItems(part()))
278
+
279
+ const badgeClass = () => {
280
+ if (part().status === 'complete') return s().mwBadgeApproved
281
+ if (part().status === 'error') return s().mwBadgeError
282
+ return s().mwBadgeDefault
283
+ }
284
+
285
+ return (
286
+ <>
287
+ <div
288
+ {...getHoverDataAttributes({
289
+ messageIds: [msg().id],
290
+ partIds: [partId()],
291
+ })}
292
+ class={s().step}
293
+ onClick={() => setExpanded(!expanded())}
294
+ onMouseEnter={() =>
295
+ props.onHoverTarget?.(
296
+ createHoverTarget({
297
+ messageIds: [msg().id],
298
+ partIds: [partId()],
299
+ origin: 'timeline',
300
+ }),
301
+ )
302
+ }
303
+ onMouseLeave={() => props.onHoverTarget?.(null)}
304
+ style={{ cursor: 'pointer' }}
305
+ >
306
+ <span class={`${s().stepPrefix} ${s().stepPrefixAssistant}`}>
307
+ Structured
308
+ </span>
309
+ <span class={`${s().mwBadge} ${badgeClass()}`}>{part().status}</span>
310
+ <span class={`${s().chevron} ${expanded() ? s().chevronOpen : ''}`}>
311
+ {'\u25B6'}
312
+ </span>
313
+ </div>
314
+ <div class={`${s().cardBody} ${expanded() ? s().cardBodyOpen : ''}`}>
315
+ <div class={`${s().cardBodyInner} ${s().stepJsonItemsCompare}`}>
316
+ <For each={jsonItems()}>
317
+ {(item) => (
318
+ <div class={s().stepJsonItem}>
319
+ <div class={s().stepJsonItemLabel}>{item.label}</div>
320
+ <div class={s().stepJsonPanel}>
321
+ <JsonTree
322
+ value={item.value}
323
+ defaultExpansionDepth={1}
324
+ copyable
325
+ />
326
+ </div>
327
+ </div>
328
+ )}
329
+ </For>
330
+ </div>
331
+ </div>
332
+ </>
333
+ )
334
+ }
335
+
204
336
  const AssistantStep: Component<{
205
337
  step: Extract<IterationStep, { kind: 'assistant' }>
338
+ onHoverTarget?: (target: HoverTarget | null) => void
206
339
  }> = (props) => {
207
340
  const styles = useStyles()
208
341
  const s = () => styles().iterationTimeline
@@ -220,7 +353,19 @@ const AssistantStep: Component<{
220
353
 
221
354
  return (
222
355
  <>
223
- <div class={`${s().step} ${isLong() ? s().stepResponseLong : ''}`}>
356
+ <div
357
+ {...getHoverDataAttributes({ messageIds: [msg().id] })}
358
+ class={`${s().step} ${isLong() ? s().stepResponseLong : ''}`}
359
+ onMouseEnter={() =>
360
+ props.onHoverTarget?.(
361
+ createHoverTarget({
362
+ messageIds: [msg().id],
363
+ origin: 'timeline',
364
+ }),
365
+ )
366
+ }
367
+ onMouseLeave={() => props.onHoverTarget?.(null)}
368
+ >
224
369
  <span class={`${s().stepPrefix} ${s().stepPrefixAssistant}`}>
225
370
  Response
226
371
  </span>
@@ -245,12 +390,16 @@ const AssistantStep: Component<{
245
390
 
246
391
  const ToolCallStep: Component<{
247
392
  step: Extract<IterationStep, { kind: 'tool_call' }>
393
+ onHoverTarget?: (target: HoverTarget | null) => void
248
394
  }> = (props) => {
249
395
  const styles = useStyles()
250
396
  const s = () => styles().iterationTimeline
251
397
  const [argsOpen, setArgsOpen] = createSignal(false)
252
398
  const [resultOpen, setResultOpen] = createSignal(false)
253
399
  const tc = () => props.step.toolCall
400
+ const callPartId = () => toolCallPartId(tc().id)
401
+ const resultPartId = () => toolResultPartId(tc().id)
402
+ const messageId = () => props.step.message.id
254
403
 
255
404
  const parsedArgs = () => {
256
405
  const raw = tc().arguments || '{}'
@@ -258,6 +407,13 @@ const ToolCallStep: Component<{
258
407
  }
259
408
 
260
409
  const hasResult = () => tc().result !== undefined
410
+ const approvalStatus = () => getApprovalStatus(tc())
411
+ const approvalBadgeClass = () => {
412
+ const status = approvalStatus()
413
+ if (status === 'approved') return s().mwBadgeApproved
414
+ if (status === 'denied') return s().mwBadgeDenied
415
+ return s().mwBadgeApproval
416
+ }
261
417
 
262
418
  const parsedResult = () => {
263
419
  if (!hasResult()) return null
@@ -270,14 +426,35 @@ const ToolCallStep: Component<{
270
426
  return (
271
427
  <>
272
428
  <div
429
+ {...getHoverDataAttributes({
430
+ messageIds: [messageId()],
431
+ partIds: [callPartId()],
432
+ })}
273
433
  class={s().step}
274
434
  onClick={() => setArgsOpen(!argsOpen())}
435
+ onMouseEnter={() =>
436
+ props.onHoverTarget?.(
437
+ createHoverTarget({
438
+ messageIds: [messageId()],
439
+ partIds: [callPartId()],
440
+ origin: 'timeline',
441
+ }),
442
+ )
443
+ }
444
+ onMouseLeave={() => props.onHoverTarget?.(null)}
275
445
  style={{ cursor: 'pointer' }}
276
446
  >
277
447
  <span class={`${s().stepPrefix} ${s().stepPrefixToolCall}`}>
278
448
  Tool Call
279
449
  </span>
280
450
  <span class={`${s().mwBadge} ${s().mwBadgeToolCall}`}>{tc().name}</span>
451
+ <Show when={approvalStatus()}>
452
+ {(status) => (
453
+ <span class={`${s().mwBadge} ${approvalBadgeClass()}`}>
454
+ {status()}
455
+ </span>
456
+ )}
457
+ </Show>
281
458
  <Show when={tc().duration !== undefined}>
282
459
  <span class={s().stepDuration}>{tc().duration}ms</span>
283
460
  </Show>
@@ -288,18 +465,28 @@ const ToolCallStep: Component<{
288
465
  <div class={`${s().cardBody} ${argsOpen() ? s().cardBodyOpen : ''}`}>
289
466
  <div class={s().cardBodyInner}>
290
467
  <div class={s().stepJsonPanel}>
291
- <JsonTree
292
- value={parsedArgs() as Record<string, unknown>}
293
- defaultExpansionDepth={0}
294
- copyable
295
- />
468
+ <JsonTree value={parsedArgs()} defaultExpansionDepth={0} copyable />
296
469
  </div>
297
470
  </div>
298
471
  </div>
299
472
  <Show when={hasResult()}>
300
473
  <div
474
+ {...getHoverDataAttributes({
475
+ messageIds: [messageId()],
476
+ partIds: [callPartId(), resultPartId()],
477
+ })}
301
478
  class={s().step}
302
479
  onClick={() => setResultOpen(!resultOpen())}
480
+ onMouseEnter={() =>
481
+ props.onHoverTarget?.(
482
+ createHoverTarget({
483
+ messageIds: [messageId()],
484
+ partIds: [callPartId(), resultPartId()],
485
+ origin: 'timeline',
486
+ }),
487
+ )
488
+ }
489
+ onMouseLeave={() => props.onHoverTarget?.(null)}
303
490
  style={{ cursor: 'pointer' }}
304
491
  >
305
492
  <span class={`${s().stepPrefix} ${s().stepPrefixToolResult}`}>
@@ -319,7 +506,7 @@ const ToolCallStep: Component<{
319
506
  <div class={s().cardBodyInner}>
320
507
  <div class={s().stepJsonPanel}>
321
508
  <JsonTree
322
- value={parsedResult() as Record<string, unknown>}
509
+ value={parsedResult()}
323
510
  defaultExpansionDepth={0}
324
511
  copyable
325
512
  />
@@ -333,6 +520,7 @@ const ToolCallStep: Component<{
333
520
 
334
521
  const ToolResultStep: Component<{
335
522
  step: Extract<IterationStep, { kind: 'tool_result' }>
523
+ onHoverTarget?: (target: HoverTarget | null) => void
336
524
  }> = (props) => {
337
525
  const styles = useStyles()
338
526
  const s = () => styles().iterationTimeline
@@ -347,8 +535,18 @@ const ToolResultStep: Component<{
347
535
  return (
348
536
  <>
349
537
  <div
538
+ {...getHoverDataAttributes({ messageIds: [msg().id] })}
350
539
  class={s().step}
351
540
  onClick={() => setIsOpen(!isOpen())}
541
+ onMouseEnter={() =>
542
+ props.onHoverTarget?.(
543
+ createHoverTarget({
544
+ messageIds: [msg().id],
545
+ origin: 'timeline',
546
+ }),
547
+ )
548
+ }
549
+ onMouseLeave={() => props.onHoverTarget?.(null)}
352
550
  style={{ cursor: 'pointer' }}
353
551
  >
354
552
  <span class={`${s().stepPrefix} ${s().stepPrefixToolResult}`}>
@@ -362,7 +560,7 @@ const ToolResultStep: Component<{
362
560
  <div class={s().cardBodyInner}>
363
561
  <div class={s().stepJsonPanel}>
364
562
  <JsonTree
365
- value={parsedContent() as Record<string, unknown>}
563
+ value={parsedContent()}
366
564
  defaultExpansionDepth={0}
367
565
  copyable
368
566
  />
@@ -395,6 +593,18 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
395
593
 
396
594
  const label = () => getIterationLabel(iter(), props.index)
397
595
  const steps = createMemo(() => buildSteps(iter(), props.messages))
596
+ const iterationPartIds = createMemo(() =>
597
+ steps().flatMap((step) => {
598
+ if (step.kind === 'structured_output') {
599
+ return [structuredOutputPartId(step.message.id)]
600
+ }
601
+ if (step.kind !== 'tool_call') return []
602
+ const callId = step.toolCall.id
603
+ return step.toolCall.result === undefined
604
+ ? [toolCallPartId(callId)]
605
+ : [toolCallPartId(callId), toolResultPartId(callId)]
606
+ }),
607
+ )
398
608
 
399
609
  /**
400
610
  * Compute delta usage for display.
@@ -443,6 +653,14 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
443
653
  return ''
444
654
  }
445
655
 
656
+ const isIterationHighlighted = () =>
657
+ iter().messageIds.some((messageId) =>
658
+ isMessageHighlighted(messageId, props.hoverTarget ?? null),
659
+ ) ||
660
+ iterationPartIds().some((partId) =>
661
+ (props.hoverTarget?.partIds ?? []).includes(partId),
662
+ )
663
+
446
664
  // Config data from this iteration
447
665
  const configSubtitle = () => {
448
666
  const { model, provider } = iter()
@@ -531,13 +749,29 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
531
749
 
532
750
  return (
533
751
  <div
534
- class={s().iterCard}
752
+ {...getHoverDataAttributes({
753
+ messageIds: iter().messageIds,
754
+ partIds: iterationPartIds(),
755
+ })}
756
+ class={`${s().iterCard} ${
757
+ isIterationHighlighted() ? s().iterCardHighlighted : ''
758
+ }`}
535
759
  style={{ 'animation-delay': `${props.index * 60}ms` }}
536
760
  >
537
761
  {/* Header */}
538
762
  <div
763
+ {...getHoverDataAttributes({ messageIds: iter().messageIds })}
539
764
  class={`${s().iterCardHeader} ${headerAccent()}`}
540
765
  onClick={() => setIsOpen(!isOpen())}
766
+ onMouseEnter={() =>
767
+ props.onHoverTarget?.(
768
+ createHoverTarget({
769
+ messageIds: iter().messageIds,
770
+ origin: 'timeline',
771
+ }),
772
+ )
773
+ }
774
+ onMouseLeave={() => props.onHoverTarget?.(null)}
541
775
  >
542
776
  <div class={s().cardHeaderContent}>
543
777
  <span class={s().iterCardTitle}>{label()}</span>
@@ -690,7 +924,7 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
690
924
  <span class={s().configPanelLabel}>Model Options</span>
691
925
  <div class={s().configJsonTreeContainer}>
692
926
  <JsonTree
693
- value={modelOptions() as Record<string, unknown>}
927
+ value={modelOptions()}
694
928
  defaultExpansionDepth={2}
695
929
  copyable
696
930
  />
@@ -719,6 +953,18 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
719
953
  step={
720
954
  step() as Extract<IterationStep, { kind: 'thinking' }>
721
955
  }
956
+ onHoverTarget={props.onHoverTarget}
957
+ />
958
+ </Match>
959
+ <Match when={step().kind === 'structured_output'}>
960
+ <StructuredOutputStep
961
+ step={
962
+ step() as Extract<
963
+ IterationStep,
964
+ { kind: 'structured_output' }
965
+ >
966
+ }
967
+ onHoverTarget={props.onHoverTarget}
722
968
  />
723
969
  </Match>
724
970
  <Match when={step().kind === 'assistant'}>
@@ -726,6 +972,7 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
726
972
  step={
727
973
  step() as Extract<IterationStep, { kind: 'assistant' }>
728
974
  }
975
+ onHoverTarget={props.onHoverTarget}
729
976
  />
730
977
  </Match>
731
978
  <Match when={step().kind === 'tool_call'}>
@@ -733,6 +980,7 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
733
980
  step={
734
981
  step() as Extract<IterationStep, { kind: 'tool_call' }>
735
982
  }
983
+ onHoverTarget={props.onHoverTarget}
736
984
  />
737
985
  </Match>
738
986
  <Match when={step().kind === 'tool_result'}>
@@ -740,6 +988,7 @@ export const IterationCard: Component<IterationCardProps> = (props) => {
740
988
  step={
741
989
  step() as Extract<IterationStep, { kind: 'tool_result' }>
742
990
  }
991
+ onHoverTarget={props.onHoverTarget}
743
992
  />
744
993
  </Match>
745
994
  </Switch>