@tanstack/ai-devtools-core 0.3.9 → 0.3.11
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.
- package/dist/Shell/{MUFU35SY.js → SHJAKIOY.js} +3873 -1906
- package/dist/Shell/{GHXECDJV.js → TWTNN4SQ.js} +3909 -1427
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/{MUFU35SY.js → SHJAKIOY.js} +3873 -1906
- package/dist/production/Shell/{GHXECDJV.js → TWTNN4SQ.js} +3909 -1427
- package/dist/production/dev.js +1 -1
- package/dist/production/index.js +1 -1
- package/dist/production/server.js +1 -1
- package/dist/server.js +1 -1
- package/package.json +3 -2
- package/src/components/ConversationDetails.tsx +166 -99
- package/src/components/conversation/ConversationHeader.tsx +57 -98
- package/src/components/conversation/ConversationTabs.tsx +12 -14
- package/src/components/conversation/IterationCard.tsx +745 -0
- package/src/components/conversation/IterationTimeline.tsx +424 -0
- package/src/components/conversation/index.ts +3 -0
- package/src/store/ai-context.tsx +485 -25
- package/src/store/ai-store.ts +8 -1
- package/src/styles/use-styles.ts +707 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { For, Index, Show, createMemo, createSignal } from 'solid-js'
|
|
2
|
+
import { JsonTree } from '@tanstack/devtools-ui'
|
|
3
|
+
import { useStyles } from '../../styles/use-styles'
|
|
4
|
+
import { formatDuration } from '../utils'
|
|
5
|
+
import { IterationCard } from './IterationCard'
|
|
6
|
+
import type { Iteration, Message } from '../../store/ai-store'
|
|
7
|
+
import type { Component } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
/** A group of iterations triggered by a single user message */
|
|
10
|
+
interface UserMessageGroup {
|
|
11
|
+
userMessage: Message | null
|
|
12
|
+
iterations: Array<Iteration>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IterationTimelineProps {
|
|
16
|
+
iterations: Array<Iteration>
|
|
17
|
+
messages: Array<Message>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const IterationTimeline: Component<IterationTimelineProps> = (props) => {
|
|
21
|
+
const styles = useStyles()
|
|
22
|
+
const s = () => styles().iterationTimeline
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Group iterations by user messages.
|
|
26
|
+
* Memoized to avoid recomputing on unrelated store changes.
|
|
27
|
+
*/
|
|
28
|
+
const groups = createMemo((): Array<UserMessageGroup> => {
|
|
29
|
+
const userMessages = props.messages.filter((m) => m.role === 'user')
|
|
30
|
+
const iters = props.iterations
|
|
31
|
+
|
|
32
|
+
if (userMessages.length === 0) {
|
|
33
|
+
return iters.length > 0 ? [{ userMessage: null, iterations: iters }] : []
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result: Array<UserMessageGroup> = []
|
|
37
|
+
|
|
38
|
+
const sortedUsers = [...userMessages].sort(
|
|
39
|
+
(a, b) => a.timestamp - b.timestamp,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
for (let u = 0; u < sortedUsers.length; u++) {
|
|
43
|
+
const currentUser = sortedUsers[u]!
|
|
44
|
+
const nextUser = sortedUsers[u + 1]
|
|
45
|
+
|
|
46
|
+
const groupIters = iters.filter((it) => {
|
|
47
|
+
if (it.startedAt < currentUser.timestamp) return false
|
|
48
|
+
if (nextUser && it.startedAt >= nextUser.timestamp) return false
|
|
49
|
+
return true
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (groupIters.length > 0) {
|
|
53
|
+
result.push({ userMessage: currentUser, iterations: groupIters })
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Catch any iterations before the first user message
|
|
58
|
+
if (sortedUsers[0]) {
|
|
59
|
+
const earlyIters = iters.filter(
|
|
60
|
+
(it) => it.startedAt < sortedUsers[0]!.timestamp,
|
|
61
|
+
)
|
|
62
|
+
if (earlyIters.length > 0) {
|
|
63
|
+
result.unshift({ userMessage: null, iterations: earlyIters })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div class={s().container}>
|
|
72
|
+
<Show
|
|
73
|
+
when={props.iterations.length > 0}
|
|
74
|
+
fallback={<div class={s().noIterations}>No iterations recorded</div>}
|
|
75
|
+
>
|
|
76
|
+
<div class={s().pipeline}>
|
|
77
|
+
<Index each={groups()}>
|
|
78
|
+
{(group) => (
|
|
79
|
+
<UserMessageGroupCard
|
|
80
|
+
group={group()}
|
|
81
|
+
allMessages={props.messages}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</Index>
|
|
85
|
+
</div>
|
|
86
|
+
</Show>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Collapsible system prompt with preview */
|
|
92
|
+
export const SystemPromptItem: Component<{ prompt: string; index: number }> = (
|
|
93
|
+
props,
|
|
94
|
+
) => {
|
|
95
|
+
const styles = useStyles()
|
|
96
|
+
const s = () => styles().iterationTimeline
|
|
97
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
98
|
+
|
|
99
|
+
const isLong = () => props.prompt.length > 120
|
|
100
|
+
const preview = () =>
|
|
101
|
+
isLong() ? props.prompt.slice(0, 120) + '...' : props.prompt
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div class={s().systemPromptCard}>
|
|
105
|
+
<div
|
|
106
|
+
class={s().systemPromptHeader}
|
|
107
|
+
onClick={() => isLong() && setExpanded(!expanded())}
|
|
108
|
+
>
|
|
109
|
+
<span class={s().systemPromptIndex}>#{props.index + 1}</span>
|
|
110
|
+
<span class={s().systemPromptPreview}>
|
|
111
|
+
{expanded() ? '' : preview()}
|
|
112
|
+
</span>
|
|
113
|
+
<Show when={isLong()}>
|
|
114
|
+
<span class={s().stepExpandToggle}>
|
|
115
|
+
{expanded() ? 'collapse' : 'expand'}
|
|
116
|
+
</span>
|
|
117
|
+
</Show>
|
|
118
|
+
</div>
|
|
119
|
+
<Show when={expanded()}>
|
|
120
|
+
<pre class={s().systemPromptFull}>{props.prompt}</pre>
|
|
121
|
+
</Show>
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Card wrapping a user message and its child iterations */
|
|
127
|
+
const UserMessageGroupCard: Component<{
|
|
128
|
+
group: UserMessageGroup
|
|
129
|
+
allMessages: Array<Message>
|
|
130
|
+
}> = (props) => {
|
|
131
|
+
const styles = useStyles()
|
|
132
|
+
const s = () => styles().iterationTimeline
|
|
133
|
+
const [isOpen, setIsOpen] = createSignal(true)
|
|
134
|
+
const [configExpanded, setConfigExpanded] = createSignal(false)
|
|
135
|
+
|
|
136
|
+
const group = () => props.group
|
|
137
|
+
const userMsg = () => group().userMessage
|
|
138
|
+
const iters = () => group().iterations
|
|
139
|
+
|
|
140
|
+
const totalDuration = createMemo(() => {
|
|
141
|
+
let sum = 0
|
|
142
|
+
for (const it of iters()) {
|
|
143
|
+
if (it.completedAt) {
|
|
144
|
+
sum += it.completedAt - it.startedAt
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return sum > 0 ? sum : undefined
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
/** Count actual tool invocations across all messages in this group */
|
|
151
|
+
const toolInvocationCounts = createMemo(() => {
|
|
152
|
+
const counts = new Map<string, number>()
|
|
153
|
+
const allMsgIds = new Set<string>()
|
|
154
|
+
for (const it of iters()) {
|
|
155
|
+
for (const id of it.messageIds) allMsgIds.add(id)
|
|
156
|
+
}
|
|
157
|
+
for (const msg of props.allMessages) {
|
|
158
|
+
if (allMsgIds.has(msg.id) && msg.toolCalls) {
|
|
159
|
+
for (const tc of msg.toolCalls) {
|
|
160
|
+
counts.set(tc.name, (counts.get(tc.name) || 0) + 1)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return counts
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const totalToolCalls = createMemo(() => {
|
|
168
|
+
let count = 0
|
|
169
|
+
for (const v of toolInvocationCounts().values()) count += v
|
|
170
|
+
return count
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Total usage across this group.
|
|
175
|
+
* Iterations store CUMULATIVE usage per request, so we take the MAX
|
|
176
|
+
* cumulative from each request group (the last iteration's value
|
|
177
|
+
* represents the total for that request).
|
|
178
|
+
*/
|
|
179
|
+
const totalUsage = createMemo(() => {
|
|
180
|
+
const maxByRequest = new Map<
|
|
181
|
+
string,
|
|
182
|
+
{ prompt: number; completion: number }
|
|
183
|
+
>()
|
|
184
|
+
for (const it of iters()) {
|
|
185
|
+
if (!it.usage) continue
|
|
186
|
+
const key = it.requestId || '__default__'
|
|
187
|
+
const existing = maxByRequest.get(key)
|
|
188
|
+
if (
|
|
189
|
+
!existing ||
|
|
190
|
+
it.usage.totalTokens > existing.prompt + existing.completion
|
|
191
|
+
) {
|
|
192
|
+
maxByRequest.set(key, {
|
|
193
|
+
prompt: it.usage.promptTokens,
|
|
194
|
+
completion: it.usage.completionTokens,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let prompt = 0
|
|
199
|
+
let completion = 0
|
|
200
|
+
for (const v of maxByRequest.values()) {
|
|
201
|
+
prompt += v.prompt
|
|
202
|
+
completion += v.completion
|
|
203
|
+
}
|
|
204
|
+
if (prompt + completion === 0) return undefined
|
|
205
|
+
return {
|
|
206
|
+
promptTokens: prompt,
|
|
207
|
+
completionTokens: completion,
|
|
208
|
+
totalTokens: prompt + completion,
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const isActive = () => iters().some((it) => !it.completedAt)
|
|
213
|
+
const hasError = () => iters().some((it) => it.finishReason === 'error')
|
|
214
|
+
const allCompleted = () =>
|
|
215
|
+
iters().every((it) => !!it.completedAt) && !hasError()
|
|
216
|
+
|
|
217
|
+
const groupAccentClass = () => {
|
|
218
|
+
if (isActive()) return s().cardActive
|
|
219
|
+
if (hasError()) return s().cardError
|
|
220
|
+
if (allCompleted()) return s().cardCompleted
|
|
221
|
+
return ''
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const userContent = () => {
|
|
225
|
+
const msg = userMsg()
|
|
226
|
+
if (!msg) return '(no user message)'
|
|
227
|
+
const text = msg.content || ''
|
|
228
|
+
return text.length > 120 ? text.slice(0, 120) + '...' : text
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Config from the first iteration of this group
|
|
232
|
+
const firstIter = createMemo(() => iters()[0])
|
|
233
|
+
|
|
234
|
+
const configSubtitle = () => {
|
|
235
|
+
const first = firstIter()
|
|
236
|
+
if (!first) return null
|
|
237
|
+
const parts: Array<string> = []
|
|
238
|
+
if (first.model) parts.push(first.model)
|
|
239
|
+
if (first.provider) parts.push(first.provider)
|
|
240
|
+
return parts.length > 0 ? parts.join(' \u00B7 ') : null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const toolNames = () => firstIter()?.toolNames || []
|
|
244
|
+
const systemPrompts = () => firstIter()?.systemPrompts || []
|
|
245
|
+
const modelOptions = () => firstIter()?.modelOptions
|
|
246
|
+
const hasModelOptions = () => {
|
|
247
|
+
const opts = modelOptions()
|
|
248
|
+
return opts && Object.keys(opts).length > 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const middlewareTransformCount = createMemo(() => {
|
|
252
|
+
let count = 0
|
|
253
|
+
for (const it of iters()) {
|
|
254
|
+
for (const ev of it.middlewareEvents) {
|
|
255
|
+
if (ev.hasTransform) count++
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return count
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const hasExpandableConfig = () =>
|
|
262
|
+
toolNames().length > 0 || systemPrompts().length > 0 || hasModelOptions()
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<div class={`${s().card} ${groupAccentClass()}`}>
|
|
266
|
+
{/* User message header */}
|
|
267
|
+
<div class={s().cardHeader} onClick={() => setIsOpen(!isOpen())}>
|
|
268
|
+
<div class={s().userBubble}>U</div>
|
|
269
|
+
<div class={s().cardHeaderContent}>
|
|
270
|
+
<span class={s().cardHeaderLabel}>{userContent()}</span>
|
|
271
|
+
{/* Config subtitle — always visible under user message */}
|
|
272
|
+
<div class={s().cardSubtitle}>
|
|
273
|
+
<Show when={configSubtitle()}>
|
|
274
|
+
<span class={s().subtitleText}>{configSubtitle()}</span>
|
|
275
|
+
</Show>
|
|
276
|
+
<Show when={toolNames().length > 0}>
|
|
277
|
+
<span class={s().subtitleBadge}>
|
|
278
|
+
{toolNames().length} tool{toolNames().length === 1 ? '' : 's'}
|
|
279
|
+
</span>
|
|
280
|
+
</Show>
|
|
281
|
+
<Show when={systemPrompts().length > 0}>
|
|
282
|
+
<span class={s().subtitleBadge}>
|
|
283
|
+
{systemPrompts().length} system prompt
|
|
284
|
+
{systemPrompts().length === 1 ? '' : 's'}
|
|
285
|
+
</span>
|
|
286
|
+
</Show>
|
|
287
|
+
<Show when={hasModelOptions()}>
|
|
288
|
+
<span class={s().subtitleBadge}>options</span>
|
|
289
|
+
</Show>
|
|
290
|
+
<Show when={middlewareTransformCount() > 0}>
|
|
291
|
+
<span class={s().subtitleBadgeWarn}>
|
|
292
|
+
{middlewareTransformCount()} middleware transform
|
|
293
|
+
{middlewareTransformCount() === 1 ? '' : 's'}
|
|
294
|
+
</span>
|
|
295
|
+
</Show>
|
|
296
|
+
<Show when={hasExpandableConfig()}>
|
|
297
|
+
<span
|
|
298
|
+
class={s().subtitleExpandToggle}
|
|
299
|
+
onClick={(e) => {
|
|
300
|
+
e.stopPropagation()
|
|
301
|
+
setConfigExpanded(!configExpanded())
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
{configExpanded() ? 'hide config' : 'show config'}
|
|
305
|
+
</span>
|
|
306
|
+
</Show>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class={s().cardHeaderBadges}>
|
|
310
|
+
<Show when={iters().length > 0}>
|
|
311
|
+
<span
|
|
312
|
+
class={`${s().badge} ${s().badgeDuration}`}
|
|
313
|
+
title={`${iters().length} ${iters().length === 1 ? 'iteration' : 'iterations'}`}
|
|
314
|
+
>
|
|
315
|
+
🔄 {iters().length}
|
|
316
|
+
</span>
|
|
317
|
+
</Show>
|
|
318
|
+
<Show when={totalToolCalls() > 0}>
|
|
319
|
+
<span
|
|
320
|
+
class={`${s().badge} ${s().badgeFinishReasonToolCalls}`}
|
|
321
|
+
title={`${totalToolCalls()} tool ${totalToolCalls() === 1 ? 'call' : 'calls'}`}
|
|
322
|
+
>
|
|
323
|
+
🔧 {totalToolCalls()}
|
|
324
|
+
</span>
|
|
325
|
+
</Show>
|
|
326
|
+
<Show when={totalDuration()}>
|
|
327
|
+
<span
|
|
328
|
+
class={`${s().badge} ${s().badgeDuration}`}
|
|
329
|
+
title={formatDuration(totalDuration())}
|
|
330
|
+
>
|
|
331
|
+
⏱️ {formatDuration(totalDuration())}
|
|
332
|
+
</span>
|
|
333
|
+
</Show>
|
|
334
|
+
<Show when={totalUsage()}>
|
|
335
|
+
<span
|
|
336
|
+
class={`${s().badge} ${s().badgeUsage}`}
|
|
337
|
+
title={`Prompt: ${totalUsage()!.promptTokens.toLocaleString()} | Completion: ${totalUsage()!.completionTokens.toLocaleString()}`}
|
|
338
|
+
>
|
|
339
|
+
🎯 {totalUsage()!.totalTokens.toLocaleString()}
|
|
340
|
+
</span>
|
|
341
|
+
</Show>
|
|
342
|
+
<Show when={isActive()}>
|
|
343
|
+
<span class={`${s().badge} ${s().badgeDuration}`}>⟳ streaming</span>
|
|
344
|
+
</Show>
|
|
345
|
+
</div>
|
|
346
|
+
<span class={`${s().chevron} ${isOpen() ? s().chevronOpen : ''}`}>
|
|
347
|
+
{'\u25B6'}
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
|
|
351
|
+
{/* Expandable config details — sits between header and iterations */}
|
|
352
|
+
<div
|
|
353
|
+
class={`${s().configPanelWrapper} ${configExpanded() ? s().configPanelWrapperOpen : ''}`}
|
|
354
|
+
>
|
|
355
|
+
<div class={s().configPanel}>
|
|
356
|
+
<div>
|
|
357
|
+
<Show when={toolNames().length > 0}>
|
|
358
|
+
<div class={s().configPanelSection}>
|
|
359
|
+
<span class={s().configPanelLabel}>Tools</span>
|
|
360
|
+
<div class={s().configToolsList}>
|
|
361
|
+
<For each={toolNames()}>
|
|
362
|
+
{(name) => (
|
|
363
|
+
<span class={s().configToolChip}>
|
|
364
|
+
{name}
|
|
365
|
+
<span class={s().configToolChipCount}>
|
|
366
|
+
{toolInvocationCounts().get(name) || 0}
|
|
367
|
+
</span>
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
</For>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</Show>
|
|
374
|
+
<Show when={systemPrompts().length > 0}>
|
|
375
|
+
<div class={s().configPanelSection}>
|
|
376
|
+
<span class={s().configPanelLabel}>
|
|
377
|
+
System Prompts ({systemPrompts().length})
|
|
378
|
+
</span>
|
|
379
|
+
<For each={systemPrompts()}>
|
|
380
|
+
{(prompt, i) => (
|
|
381
|
+
<SystemPromptItem prompt={prompt} index={i()} />
|
|
382
|
+
)}
|
|
383
|
+
</For>
|
|
384
|
+
</div>
|
|
385
|
+
</Show>
|
|
386
|
+
<Show when={hasModelOptions()}>
|
|
387
|
+
<div class={s().configPanelSection}>
|
|
388
|
+
<span class={s().configPanelLabel}>Model Options</span>
|
|
389
|
+
<div class={s().configJsonTreeContainer}>
|
|
390
|
+
<JsonTree
|
|
391
|
+
value={modelOptions() as Record<string, unknown>}
|
|
392
|
+
defaultExpansionDepth={2}
|
|
393
|
+
copyable
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
</div>
|
|
397
|
+
</Show>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{/* Iterations list — full width */}
|
|
403
|
+
<div class={`${s().cardBody} ${isOpen() ? s().cardBodyOpen : ''}`}>
|
|
404
|
+
<div class={s().cardBodyInner}>
|
|
405
|
+
<div class={s().iterList}>
|
|
406
|
+
<For each={iters()}>
|
|
407
|
+
{(iteration, index) => (
|
|
408
|
+
<IterationCard
|
|
409
|
+
iteration={iteration}
|
|
410
|
+
previousIteration={
|
|
411
|
+
index() > 0 ? iters()[index() - 1] : undefined
|
|
412
|
+
}
|
|
413
|
+
messages={props.allMessages}
|
|
414
|
+
index={index()}
|
|
415
|
+
isLast={index() === iters().length - 1}
|
|
416
|
+
/>
|
|
417
|
+
)}
|
|
418
|
+
</For>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
@@ -6,3 +6,6 @@ export { MessagesTab } from './MessagesTab'
|
|
|
6
6
|
export { ChunksTab } from './ChunksTab'
|
|
7
7
|
export { SummariesTab } from './SummariesTab'
|
|
8
8
|
export { ActivityEventsTab } from './ActivityEventsTab'
|
|
9
|
+
|
|
10
|
+
export { IterationTimeline } from './IterationTimeline'
|
|
11
|
+
export { IterationCard } from './IterationCard'
|