@tanstack/ai-devtools-core 0.5.17 → 0.5.20

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.
@@ -7,6 +7,14 @@ import {
7
7
  getHookUnseenEventCount,
8
8
  } from '../../store/hook-registry'
9
9
  import { IterationTimeline } from '../conversation'
10
+ import { SubagentStepsCard } from '../conversation/IterationTimeline'
11
+ import {
12
+ childRequestAgents,
13
+ collectServerSteps,
14
+ collectSubagents,
15
+ snapshotTurns,
16
+ } from '../../store/subagent-steps'
17
+ import type { SubagentInfo } from '../../store/subagent-steps'
10
18
  import { FixtureNamePopover } from './FixtureNamePopover'
11
19
  import { ToolFixtureForm } from './ToolFixtureForm'
12
20
  import {
@@ -48,7 +56,12 @@ import type {
48
56
  ToolFixtureMessage,
49
57
  ToolFixtureRecord,
50
58
  } from '../../store/hook-registry'
51
- import type { Conversation, Message, ToolCall } from '../../store/ai-store'
59
+ import type {
60
+ Conversation,
61
+ Iteration,
62
+ Message,
63
+ ToolCall,
64
+ } from '../../store/ai-store'
52
65
  import type { Component, Setter } from 'solid-js'
53
66
 
54
67
  type DetailTab =
@@ -67,6 +80,8 @@ interface PreviewMessage {
67
80
  content: string
68
81
  parts: Array<PreviewPart>
69
82
  sourceMessage?: ToolFixtureMessage
83
+ /** Agent path when a subagent's server run wrote this message. */
84
+ agent?: string
70
85
  }
71
86
 
72
87
  interface PreviewPart {
@@ -81,7 +96,12 @@ interface PreviewPart {
81
96
  | 'tool-result'
82
97
  | 'structured-output'
83
98
  | 'media'
99
+ | 'subagent'
84
100
  fixture?: PreviewToolFixture
101
+ /** Set on a subagent header row. */
102
+ subagentId?: string
103
+ /** Set on a row that belongs to a subagent. */
104
+ subagentPath?: string
85
105
  }
86
106
 
87
107
  interface PreviewToolFixture {
@@ -115,6 +135,7 @@ interface PreviewPartSource {
115
135
  errorMessage?: unknown
116
136
  source?: unknown
117
137
  metadata?: unknown
138
+ subagent?: unknown
118
139
  }
119
140
 
120
141
  export const HookDetails: Component = () => {
@@ -171,14 +192,50 @@ export const HookDetails: Component = () => {
171
192
  }
172
193
  })
173
194
 
195
+ const subagents = createMemo((): Array<SubagentInfo> => {
196
+ const messages = hook()?.state.messages
197
+ return Array.isArray(messages) ? collectSubagents(messages) : []
198
+ })
199
+
200
+ const serverSteps = createMemo(() =>
201
+ collectServerSteps(
202
+ conversation(),
203
+ Object.values(state.conversations),
204
+ subagents(),
205
+ ),
206
+ )
207
+
208
+ const turns = createMemo((): Array<Message> => {
209
+ const messages = hook()?.state.messages
210
+ return Array.isArray(messages) ? snapshotTurns(messages) : []
211
+ })
212
+
174
213
  const previewMessages = createMemo(() => {
175
214
  const activeHook = hook()
176
215
  if (!activeHook) return []
177
216
  const snapshotMessages = messagesFromSnapshot(activeHook.state)
217
+ // The snapshot is what the user sees. The server conversation can miss
218
+ // turns (a routed turn has no parent server run), so prefer the snapshot.
219
+ if (snapshotMessages.length > 0 && subagents().length > 0) {
220
+ return snapshotMessages
221
+ }
178
222
  const conversationMessages = conversation()?.messages ?? []
179
223
  if (conversationMessages.length > 0) {
224
+ // A child chat() re-sends the history. Its user messages are copies.
225
+ const childAgents = childRequestAgents(
226
+ conversation()?.iterations ?? [],
227
+ subagents(),
228
+ )
229
+ const agentOf = (message: Message) =>
230
+ message.requestId ? childAgents.get(message.requestId) : undefined
180
231
  return mergePreviewMessagesForUserView(
181
- conversationMessages.map(messageFromConversation),
232
+ conversationMessages
233
+ .filter((message) => !(message.role === 'user' && agentOf(message)))
234
+ .map((message) => {
235
+ const preview = messageFromConversation(message)
236
+ const agent = agentOf(message)
237
+ return agent ? { ...preview, agent } : preview
238
+ }),
182
239
  snapshotMessages,
183
240
  )
184
241
  }
@@ -314,6 +371,9 @@ export const HookDetails: Component = () => {
314
371
  hook={activeHook()}
315
372
  conversation={conversation()}
316
373
  messages={previewMessages()}
374
+ subagents={subagents()}
375
+ steps={serverSteps()}
376
+ turns={turns()}
317
377
  hoverTarget={hoverTarget()}
318
378
  onHoverTarget={setHoverTarget}
319
379
  />
@@ -656,13 +716,17 @@ const ConversationPanel: Component<{
656
716
  hook: HookRecord
657
717
  conversation?: Conversation
658
718
  messages: Array<PreviewMessage>
719
+ subagents: Array<SubagentInfo>
720
+ /** Server steps of the parent and of every child agent. */
721
+ steps: { iterations: Array<Iteration>; messages: Array<Message> }
722
+ turns: Array<Message>
659
723
  hoverTarget: HoverTarget | null
660
724
  onHoverTarget: (target: HoverTarget | null) => void
661
725
  }> = (props) => {
662
726
  const styles = useStyles()
663
727
  return (
664
728
  <Show
665
- when={props.conversation}
729
+ when={props.conversation || props.steps.iterations.length > 0}
666
730
  fallback={
667
731
  <Show
668
732
  when={props.messages.length > 0}
@@ -679,31 +743,33 @@ const ConversationPanel: Component<{
679
743
  >
680
744
  <HookMessageTimeline
681
745
  messages={props.messages}
746
+ subagents={props.subagents}
682
747
  hoverTarget={props.hoverTarget}
683
748
  onHoverTarget={props.onHoverTarget}
684
749
  />
685
750
  </Show>
686
751
  }
687
752
  >
688
- {(conversation) => (
689
- <Show
690
- when={conversation().iterations.length > 0}
691
- fallback={
692
- <HookMessageTimeline
693
- messages={props.messages}
694
- hoverTarget={props.hoverTarget}
695
- onHoverTarget={props.onHoverTarget}
696
- />
697
- }
698
- >
699
- <IterationTimeline
700
- iterations={conversation().iterations}
701
- messages={conversation().messages}
753
+ <Show
754
+ when={props.steps.iterations.length > 0}
755
+ fallback={
756
+ <HookMessageTimeline
757
+ messages={props.messages}
758
+ subagents={props.subagents}
702
759
  hoverTarget={props.hoverTarget}
703
760
  onHoverTarget={props.onHoverTarget}
704
761
  />
705
- </Show>
706
- )}
762
+ }
763
+ >
764
+ <IterationTimeline
765
+ iterations={props.steps.iterations}
766
+ messages={props.steps.messages}
767
+ subagents={props.subagents}
768
+ turns={props.turns}
769
+ hoverTarget={props.hoverTarget}
770
+ onHoverTarget={props.onHoverTarget}
771
+ />
772
+ </Show>
707
773
  </Show>
708
774
  )
709
775
  }
@@ -765,7 +831,10 @@ const MessagesPreview: Component<{
765
831
  </Show>
766
832
  <For each={props.messages}>
767
833
  {(message) => {
768
- const visibleParts = () => visiblePreviewPartsForMessage(message)
834
+ const visibleParts = () =>
835
+ visiblePreviewPartsForMessage(message).filter(
836
+ (part) => !hiddenInUserView(message, part),
837
+ )
769
838
  return (
770
839
  <div
771
840
  {...getHoverDataAttributes({
@@ -795,7 +864,7 @@ const MessagesPreview: Component<{
795
864
  onMouseLeave={() => props.onHoverTarget(null)}
796
865
  >
797
866
  <div class={styles().hookDetails.messageRole}>
798
- {message.role}
867
+ {roleLabel(message)}
799
868
  </div>
800
869
  <Show when={message.content}>
801
870
  <div class={styles().hookDetails.messageContent}>
@@ -917,6 +986,7 @@ const MessagesPreview: Component<{
917
986
 
918
987
  const HookMessageTimeline: Component<{
919
988
  messages: Array<PreviewMessage>
989
+ subagents: Array<SubagentInfo>
920
990
  hoverTarget: HoverTarget | null
921
991
  onHoverTarget: (target: HoverTarget | null) => void
922
992
  }> = (props) => {
@@ -954,67 +1024,94 @@ const HookMessageTimeline: Component<{
954
1024
  }
955
1025
  onMouseLeave={() => props.onHoverTarget(null)}
956
1026
  >
957
- <div class={styles().hookDetails.messageRole}>{message.role}</div>
1027
+ <div class={styles().hookDetails.messageRole}>
1028
+ {roleLabel(message)}
1029
+ </div>
958
1030
  <Show when={message.content}>
959
1031
  <div class={styles().hookDetails.messageContent}>
960
1032
  {message.content}
961
1033
  </div>
962
1034
  </Show>
963
- <For each={visiblePreviewPartsForMessage(message)}>
1035
+ <For
1036
+ each={visiblePreviewPartsForMessage(message).filter(
1037
+ (part) => part.subagentId || !part.subagentPath,
1038
+ )}
1039
+ >
964
1040
  {(part) => (
965
- <div
966
- {...getHoverDataAttributes({
967
- messageIds: [message.id],
968
- partIds: [part.id],
969
- })}
970
- class={styles().hookDetails.previewPart}
971
- data-testid="ai-devtools-timeline-part"
972
- data-part-id={part.id}
973
- data-part-kind={part.kind}
974
- >
975
- <span class={styles().hookDetails.previewPartLabel}>
976
- {part.label}
977
- </span>
978
- <Show
979
- when={part.jsonItems?.length}
980
- fallback={
981
- <span class={styles().hookDetails.previewPartContent}>
982
- {part.content}
983
- </span>
984
- }
985
- >
1041
+ <Show
1042
+ when={props.subagents.find(
1043
+ (agent) => agent.id === part.subagentId,
1044
+ )}
1045
+ fallback={
986
1046
  <div
987
- class={`${styles().hookDetails.previewJsonItems} ${
988
- part.kind === 'structured-output'
989
- ? styles().hookDetails.previewJsonItemsCompare
990
- : ''
991
- }`}
1047
+ {...getHoverDataAttributes({
1048
+ messageIds: [message.id],
1049
+ partIds: [part.id],
1050
+ })}
1051
+ class={styles().hookDetails.previewPart}
1052
+ data-testid="ai-devtools-timeline-part"
1053
+ data-part-id={part.id}
1054
+ data-part-kind={part.kind}
992
1055
  >
993
- <For each={part.jsonItems}>
994
- {(item) => (
995
- <div class={styles().hookDetails.previewJsonItem}>
996
- <span
997
- class={
998
- styles().hookDetails.previewJsonItemLabel
999
- }
1000
- >
1001
- {item.label}
1002
- </span>
1003
- <div
1004
- class={styles().hookDetails.previewJsonPanel}
1005
- >
1006
- <JsonTree
1007
- value={item.value}
1008
- defaultExpansionDepth={1}
1009
- copyable
1010
- />
1011
- </div>
1012
- </div>
1013
- )}
1014
- </For>
1056
+ <span class={styles().hookDetails.previewPartLabel}>
1057
+ {part.label}
1058
+ </span>
1059
+ <Show
1060
+ when={part.jsonItems?.length}
1061
+ fallback={
1062
+ <span
1063
+ class={styles().hookDetails.previewPartContent}
1064
+ >
1065
+ {part.content}
1066
+ </span>
1067
+ }
1068
+ >
1069
+ <div
1070
+ class={`${styles().hookDetails.previewJsonItems} ${
1071
+ part.kind === 'structured-output'
1072
+ ? styles().hookDetails.previewJsonItemsCompare
1073
+ : ''
1074
+ }`}
1075
+ >
1076
+ <For each={part.jsonItems}>
1077
+ {(item) => (
1078
+ <div
1079
+ class={styles().hookDetails.previewJsonItem}
1080
+ >
1081
+ <span
1082
+ class={
1083
+ styles().hookDetails.previewJsonItemLabel
1084
+ }
1085
+ >
1086
+ {item.label}
1087
+ </span>
1088
+ <div
1089
+ class={
1090
+ styles().hookDetails.previewJsonPanel
1091
+ }
1092
+ >
1093
+ <JsonTree
1094
+ value={item.value}
1095
+ defaultExpansionDepth={1}
1096
+ copyable
1097
+ />
1098
+ </div>
1099
+ </div>
1100
+ )}
1101
+ </For>
1102
+ </div>
1103
+ </Show>
1015
1104
  </div>
1016
- </Show>
1017
- </div>
1105
+ }
1106
+ >
1107
+ {(agent) => (
1108
+ <SubagentStepsCard
1109
+ agent={agent()}
1110
+ hoverTarget={props.hoverTarget}
1111
+ onHoverTarget={props.onHoverTarget}
1112
+ />
1113
+ )}
1114
+ </Show>
1018
1115
  )}
1019
1116
  </For>
1020
1117
  </div>
@@ -1025,6 +1122,17 @@ const HookMessageTimeline: Component<{
1025
1122
  )
1026
1123
  }
1027
1124
 
1125
+ /** The role, plus the agent path for a subagent's server message. */
1126
+ function roleLabel(message: PreviewMessage): string {
1127
+ return message.agent ? `${message.role} (${message.agent})` : message.role
1128
+ }
1129
+
1130
+ /** The user view shows a subagent's text and tool outputs only. */
1131
+ function hiddenInUserView(message: PreviewMessage, part: PreviewPart): boolean {
1132
+ if (!message.agent && !part.subagentPath) return false
1133
+ return part.kind === 'thinking' || part.kind === 'tool-call'
1134
+ }
1135
+
1028
1136
  const ToolsView: Component<{ hook: HookRecord }> = (props) => {
1029
1137
  const { state, applyToolFixture, deleteToolFixture } = useAIStore()
1030
1138
  const styles = useStyles()
@@ -1426,8 +1534,8 @@ function partsFromMessageParts(
1426
1534
  parts: Array<MessagePart>,
1427
1535
  messageId: string,
1428
1536
  ): Array<PreviewPart> {
1429
- return parts.map((part, index) =>
1430
- previewPartFromRecord(part, index, messageId),
1537
+ return parts.flatMap((part, index) =>
1538
+ previewPartsFromRecord(part, index, messageId),
1431
1539
  )
1432
1540
  }
1433
1541
 
@@ -1435,13 +1543,71 @@ function partsFromUnknown(
1435
1543
  parts: Array<unknown>,
1436
1544
  messageId?: string,
1437
1545
  ): Array<PreviewPart> {
1438
- return parts
1439
- .map((part, index) =>
1440
- isRecord(part)
1441
- ? previewPartFromRecord(part, index, messageId)
1442
- : undefined,
1443
- )
1444
- .filter(isPreviewPart)
1546
+ return parts.flatMap((part, index) =>
1547
+ isRecord(part) ? previewPartsFromRecord(part, index, messageId) : [],
1548
+ )
1549
+ }
1550
+
1551
+ /**
1552
+ * One message part as preview rows. A `subagent` part becomes a header row,
1553
+ * then the child's own parts (reasoning, tool calls, results, text), each
1554
+ * labeled with the agent path. Nested children expand the same way.
1555
+ */
1556
+ function previewPartsFromRecord(
1557
+ part: PreviewPartSource,
1558
+ index: number,
1559
+ messageId?: string,
1560
+ path?: string,
1561
+ ): Array<PreviewPart> {
1562
+ if (part.type === 'subagent' && isRecord(part.subagent)) {
1563
+ return subagentPreviewParts(part.subagent, index, path)
1564
+ }
1565
+ const preview = previewPartFromRecord(part, index, messageId)
1566
+ if (!path) return [preview]
1567
+ // A child's tool call ran on that child, so this hook cannot replay it.
1568
+ const { fixture: _fixture, ...childPart } = preview
1569
+ return [
1570
+ {
1571
+ ...childPart,
1572
+ id: `${path}:${messageId ?? index}:${preview.id}`,
1573
+ label: `${path} > ${preview.label}`,
1574
+ subagentPath: path,
1575
+ },
1576
+ ]
1577
+ }
1578
+
1579
+ function subagentPreviewParts(
1580
+ subagent: Record<string, unknown>,
1581
+ index: number,
1582
+ parentPath?: string,
1583
+ ): Array<PreviewPart> {
1584
+ const name = typeof subagent.name === 'string' ? subagent.name : 'subagent'
1585
+ const status =
1586
+ typeof subagent.status === 'string' ? subagent.status : undefined
1587
+ const id = typeof subagent.id === 'string' ? subagent.id : `${index}`
1588
+ const error = isRecord(subagent.error) ? subagent.error.message : undefined
1589
+ const path = parentPath ? `${parentPath} > ${name}` : name
1590
+ const header: PreviewPart = {
1591
+ id: `subagent:${id}`,
1592
+ label: status ? `subagent ${path} - ${status}` : `subagent ${path}`,
1593
+ ...(typeof error === 'string' ? { content: error } : {}),
1594
+ kind: 'subagent',
1595
+ subagentId: id,
1596
+ }
1597
+ const messages = Array.isArray(subagent.messages) ? subagent.messages : []
1598
+ return [
1599
+ header,
1600
+ ...messages.flatMap((message, messageIndex) => {
1601
+ if (!isRecord(message) || !Array.isArray(message.parts)) return []
1602
+ const childMessageId =
1603
+ typeof message.id === 'string' ? message.id : `${id}:${messageIndex}`
1604
+ return message.parts.flatMap((child, childIndex) =>
1605
+ isRecord(child)
1606
+ ? previewPartsFromRecord(child, childIndex, childMessageId, path)
1607
+ : [],
1608
+ )
1609
+ }),
1610
+ ]
1445
1611
  }
1446
1612
 
1447
1613
  function previewPartFromRecord(
@@ -1789,10 +1955,6 @@ function isPreviewMessage(
1789
1955
  return Boolean(value)
1790
1956
  }
1791
1957
 
1792
- function isPreviewPart(value: PreviewPart | undefined): value is PreviewPart {
1793
- return Boolean(value)
1794
- }
1795
-
1796
1958
  function isRecord(value: unknown): value is Record<string, unknown> {
1797
1959
  return typeof value === 'object' && value !== null
1798
1960
  }
@@ -174,6 +174,8 @@ export interface MiddlewareEvent {
174
174
  export interface Iteration {
175
175
  /** The requestId this iteration belongs to (unique per chat() call) */
176
176
  requestId?: string
177
+ /** The chat run. A subagent run is `<parentRunId>:<subagentRunId>`. */
178
+ runId?: string
177
179
  index: number
178
180
  messageId: string
179
181
  startedAt: number
@@ -2816,6 +2818,7 @@ export const AIProvider: ParentComponent = (props) => {
2816
2818
 
2817
2819
  const newIteration: Iteration = {
2818
2820
  requestId,
2821
+ ...(e.payload.runId ? { runId: e.payload.runId } : {}),
2819
2822
  index: iteration,
2820
2823
  messageId,
2821
2824
  startedAt: e.payload.timestamp,