@tanstack/ai-devtools-core 0.3.8 → 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,745 @@
|
|
|
1
|
+
import {
|
|
2
|
+
For,
|
|
3
|
+
Index,
|
|
4
|
+
Match,
|
|
5
|
+
Show,
|
|
6
|
+
Switch,
|
|
7
|
+
createMemo,
|
|
8
|
+
createSignal,
|
|
9
|
+
} from 'solid-js'
|
|
10
|
+
import { JsonTree } from '@tanstack/devtools-ui'
|
|
11
|
+
import { useStyles } from '../../styles/use-styles'
|
|
12
|
+
import { formatDuration } from '../utils'
|
|
13
|
+
import { SystemPromptItem } from './IterationTimeline'
|
|
14
|
+
import type {
|
|
15
|
+
Iteration,
|
|
16
|
+
Message,
|
|
17
|
+
MiddlewareEvent,
|
|
18
|
+
ToolCall,
|
|
19
|
+
} from '../../store/ai-store'
|
|
20
|
+
import type { Component } from 'solid-js'
|
|
21
|
+
|
|
22
|
+
interface IterationCardProps {
|
|
23
|
+
iteration: Iteration
|
|
24
|
+
previousIteration?: Iteration
|
|
25
|
+
messages: Array<Message>
|
|
26
|
+
index: number
|
|
27
|
+
isLast: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Step types ---
|
|
31
|
+
|
|
32
|
+
type IterationStep =
|
|
33
|
+
| { kind: 'middleware'; event: MiddlewareEvent }
|
|
34
|
+
| { kind: 'thinking'; message: Message }
|
|
35
|
+
| { kind: 'assistant'; message: Message }
|
|
36
|
+
| { kind: 'tool_call'; toolCall: ToolCall; message: Message }
|
|
37
|
+
| { kind: 'tool_result'; message: Message }
|
|
38
|
+
|
|
39
|
+
// --- Helpers ---
|
|
40
|
+
|
|
41
|
+
function getIterationLabel(iter: Iteration, displayIndex: number): string {
|
|
42
|
+
if (!iter.completedAt) return `Iteration ${displayIndex} — Generating...`
|
|
43
|
+
if (iter.finishReason === 'error') return `Iteration ${displayIndex} — Error`
|
|
44
|
+
return `Iteration ${displayIndex}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build steps in insertion order — no timestamp sorting.
|
|
49
|
+
* Events are emitted in order by the server, so we respect that order.
|
|
50
|
+
*/
|
|
51
|
+
function buildSteps(
|
|
52
|
+
iter: Iteration,
|
|
53
|
+
allMessages: Array<Message>,
|
|
54
|
+
): Array<IterationStep> {
|
|
55
|
+
const steps: Array<IterationStep> = []
|
|
56
|
+
|
|
57
|
+
// 1. Middleware events come first (they happen before/during generation)
|
|
58
|
+
for (const event of iter.middlewareEvents) {
|
|
59
|
+
steps.push({ kind: 'middleware', event })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Messages in their natural order from the store
|
|
63
|
+
const iterMessages = allMessages.filter(
|
|
64
|
+
(m) => iter.messageIds.includes(m.id) && m.role !== 'user',
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for (const msg of iterMessages) {
|
|
68
|
+
if (msg.role === 'assistant') {
|
|
69
|
+
// Show thinking/reasoning as its own step before text content
|
|
70
|
+
if (msg.thinkingContent) {
|
|
71
|
+
steps.push({ kind: 'thinking', message: msg })
|
|
72
|
+
}
|
|
73
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
74
|
+
for (const tc of msg.toolCalls) {
|
|
75
|
+
steps.push({ kind: 'tool_call', toolCall: tc, message: msg })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (msg.content) {
|
|
79
|
+
steps.push({ kind: 'assistant', message: msg })
|
|
80
|
+
}
|
|
81
|
+
} else if (msg.role === 'tool') {
|
|
82
|
+
steps.push({ kind: 'tool_result', message: msg })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return steps
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function truncate(str: string, max: number): string {
|
|
90
|
+
if (str.length <= max) return str
|
|
91
|
+
return str.slice(0, max) + '...'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tryParseJson(str: string): unknown | null {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(str)
|
|
97
|
+
} catch {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Step renderers ---
|
|
103
|
+
|
|
104
|
+
const MiddlewareStep: Component<{
|
|
105
|
+
step: Extract<IterationStep, { kind: 'middleware' }>
|
|
106
|
+
}> = (props) => {
|
|
107
|
+
const styles = useStyles()
|
|
108
|
+
const s = () => styles().iterationTimeline
|
|
109
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
110
|
+
const ev = () => props.step.event
|
|
111
|
+
|
|
112
|
+
const badgeClass = () => {
|
|
113
|
+
if (ev().wasDropped) return s().mwBadgeError
|
|
114
|
+
if (ev().hasTransform) return s().mwBadgeTransform
|
|
115
|
+
return s().mwBadgeDefault
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const suffix = () => {
|
|
119
|
+
if (ev().wasDropped) return 'DROP'
|
|
120
|
+
if (ev().hookName === 'onChunk' && ev().hasTransform) return 'TRANSFORM'
|
|
121
|
+
if (ev().hookName === 'onConfig' && ev().hasTransform) return 'TRANSFORM'
|
|
122
|
+
if (ev().hookName === 'onBeforeToolCall' && ev().hasTransform)
|
|
123
|
+
return 'DECISION'
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hasChanges = () =>
|
|
128
|
+
ev().configChanges && Object.keys(ev().configChanges!).length > 0
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<div class={s().step}>
|
|
133
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixMiddleware}`}>
|
|
134
|
+
Middleware
|
|
135
|
+
</span>
|
|
136
|
+
<span class={`${s().mwBadge} ${badgeClass()}`}>
|
|
137
|
+
{ev().middlewareName}
|
|
138
|
+
</span>
|
|
139
|
+
<span class={s().mwHook}>{ev().hookName}</span>
|
|
140
|
+
<Show when={ev().duration !== undefined}>
|
|
141
|
+
<span class={s().stepDuration}>{ev().duration}ms</span>
|
|
142
|
+
</Show>
|
|
143
|
+
<Show when={suffix()}>
|
|
144
|
+
<span class={s().mwSuffix}>{suffix()}</span>
|
|
145
|
+
</Show>
|
|
146
|
+
<Show when={hasChanges()}>
|
|
147
|
+
<span
|
|
148
|
+
class={s().stepExpandToggle}
|
|
149
|
+
onClick={() => setExpanded(!expanded())}
|
|
150
|
+
>
|
|
151
|
+
{expanded() ? 'hide changes' : 'show changes'}
|
|
152
|
+
</span>
|
|
153
|
+
</Show>
|
|
154
|
+
</div>
|
|
155
|
+
<Show when={expanded() && hasChanges()}>
|
|
156
|
+
<div class={s().mwChangesContainer}>
|
|
157
|
+
<JsonTree
|
|
158
|
+
value={ev().configChanges as Record<string, unknown>}
|
|
159
|
+
defaultExpansionDepth={2}
|
|
160
|
+
copyable
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
</Show>
|
|
164
|
+
</>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ThinkingStep: Component<{
|
|
169
|
+
step: Extract<IterationStep, { kind: 'thinking' }>
|
|
170
|
+
}> = (props) => {
|
|
171
|
+
const styles = useStyles()
|
|
172
|
+
const s = () => styles().iterationTimeline
|
|
173
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
174
|
+
const msg = () => props.step.message
|
|
175
|
+
|
|
176
|
+
const thinkingText = () => msg().thinkingContent || ''
|
|
177
|
+
const preview = () => truncate(thinkingText(), 150)
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<div class={s().step}>
|
|
182
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixThinking}`}>
|
|
183
|
+
Thinking
|
|
184
|
+
</span>
|
|
185
|
+
<span class={s().stepContent}>{preview() || '(empty)'}</span>
|
|
186
|
+
<Show when={thinkingText().length > 150}>
|
|
187
|
+
<span
|
|
188
|
+
class={s().stepExpandToggle}
|
|
189
|
+
onClick={() => setExpanded(!expanded())}
|
|
190
|
+
>
|
|
191
|
+
{expanded() ? 'hide' : 'show full'}
|
|
192
|
+
</span>
|
|
193
|
+
</Show>
|
|
194
|
+
</div>
|
|
195
|
+
<Show when={expanded()}>
|
|
196
|
+
<div class={s().thinkingDetail}>{thinkingText()}</div>
|
|
197
|
+
</Show>
|
|
198
|
+
</>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const AssistantStep: Component<{
|
|
203
|
+
step: Extract<IterationStep, { kind: 'assistant' }>
|
|
204
|
+
}> = (props) => {
|
|
205
|
+
const styles = useStyles()
|
|
206
|
+
const s = () => styles().iterationTimeline
|
|
207
|
+
const [expanded, setExpanded] = createSignal(false)
|
|
208
|
+
const msg = () => props.step.message
|
|
209
|
+
|
|
210
|
+
const contentLength = () => (msg().content || '').length
|
|
211
|
+
const isLong = () => contentLength() > 200
|
|
212
|
+
|
|
213
|
+
const preview = () => {
|
|
214
|
+
const text = msg().content || ''
|
|
215
|
+
if (text.length <= 200) return text
|
|
216
|
+
return truncate(text, 200)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<>
|
|
221
|
+
<div class={`${s().step} ${isLong() ? s().stepResponseLong : ''}`}>
|
|
222
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixAssistant}`}>
|
|
223
|
+
Response
|
|
224
|
+
</span>
|
|
225
|
+
<span class={isLong() ? s().stepContentLong : s().stepContent}>
|
|
226
|
+
{preview() || '(empty)'}
|
|
227
|
+
</span>
|
|
228
|
+
<Show when={contentLength() > 200}>
|
|
229
|
+
<span
|
|
230
|
+
class={s().stepExpandToggle}
|
|
231
|
+
onClick={() => setExpanded(!expanded())}
|
|
232
|
+
>
|
|
233
|
+
{expanded() ? 'hide' : 'show full'}
|
|
234
|
+
</span>
|
|
235
|
+
</Show>
|
|
236
|
+
</div>
|
|
237
|
+
<Show when={expanded()}>
|
|
238
|
+
<div class={s().responseDetail}>{msg().content}</div>
|
|
239
|
+
</Show>
|
|
240
|
+
</>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const ToolCallStep: Component<{
|
|
245
|
+
step: Extract<IterationStep, { kind: 'tool_call' }>
|
|
246
|
+
}> = (props) => {
|
|
247
|
+
const styles = useStyles()
|
|
248
|
+
const s = () => styles().iterationTimeline
|
|
249
|
+
const [argsOpen, setArgsOpen] = createSignal(false)
|
|
250
|
+
const [resultOpen, setResultOpen] = createSignal(false)
|
|
251
|
+
const tc = () => props.step.toolCall
|
|
252
|
+
|
|
253
|
+
const parsedArgs = () => {
|
|
254
|
+
const raw = tc().arguments || '{}'
|
|
255
|
+
return tryParseJson(raw) || raw
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hasResult = () => tc().result !== undefined
|
|
259
|
+
|
|
260
|
+
const parsedResult = () => {
|
|
261
|
+
if (!hasResult()) return null
|
|
262
|
+
if (typeof tc().result === 'string') {
|
|
263
|
+
return tryParseJson(tc().result as string) || tc().result
|
|
264
|
+
}
|
|
265
|
+
return tc().result
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return (
|
|
269
|
+
<>
|
|
270
|
+
<div
|
|
271
|
+
class={s().step}
|
|
272
|
+
onClick={() => setArgsOpen(!argsOpen())}
|
|
273
|
+
style={{ cursor: 'pointer' }}
|
|
274
|
+
>
|
|
275
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixToolCall}`}>
|
|
276
|
+
Tool Call
|
|
277
|
+
</span>
|
|
278
|
+
<span class={`${s().mwBadge} ${s().mwBadgeToolCall}`}>{tc().name}</span>
|
|
279
|
+
<Show when={tc().duration !== undefined}>
|
|
280
|
+
<span class={s().stepDuration}>{tc().duration}ms</span>
|
|
281
|
+
</Show>
|
|
282
|
+
<span class={`${s().chevron} ${argsOpen() ? s().chevronOpen : ''}`}>
|
|
283
|
+
{'\u25B6'}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<div class={`${s().cardBody} ${argsOpen() ? s().cardBodyOpen : ''}`}>
|
|
287
|
+
<div class={s().cardBodyInner}>
|
|
288
|
+
<div class={s().stepJsonPanel}>
|
|
289
|
+
<JsonTree
|
|
290
|
+
value={parsedArgs() as Record<string, unknown>}
|
|
291
|
+
defaultExpansionDepth={0}
|
|
292
|
+
copyable
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<Show when={hasResult()}>
|
|
298
|
+
<div
|
|
299
|
+
class={s().step}
|
|
300
|
+
onClick={() => setResultOpen(!resultOpen())}
|
|
301
|
+
style={{ cursor: 'pointer' }}
|
|
302
|
+
>
|
|
303
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixToolResult}`}>
|
|
304
|
+
Result
|
|
305
|
+
</span>
|
|
306
|
+
<span class={`${s().mwBadge} ${s().mwBadgeToolResult}`}>
|
|
307
|
+
{tc().name}
|
|
308
|
+
</span>
|
|
309
|
+
<Show when={tc().duration !== undefined}>
|
|
310
|
+
<span class={s().stepDuration}>{tc().duration}ms</span>
|
|
311
|
+
</Show>
|
|
312
|
+
<span class={`${s().chevron} ${resultOpen() ? s().chevronOpen : ''}`}>
|
|
313
|
+
{'\u25B6'}
|
|
314
|
+
</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div class={`${s().cardBody} ${resultOpen() ? s().cardBodyOpen : ''}`}>
|
|
317
|
+
<div class={s().cardBodyInner}>
|
|
318
|
+
<div class={s().stepJsonPanel}>
|
|
319
|
+
<JsonTree
|
|
320
|
+
value={parsedResult() as Record<string, unknown>}
|
|
321
|
+
defaultExpansionDepth={0}
|
|
322
|
+
copyable
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</Show>
|
|
328
|
+
</>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const ToolResultStep: Component<{
|
|
333
|
+
step: Extract<IterationStep, { kind: 'tool_result' }>
|
|
334
|
+
}> = (props) => {
|
|
335
|
+
const styles = useStyles()
|
|
336
|
+
const s = () => styles().iterationTimeline
|
|
337
|
+
const [isOpen, setIsOpen] = createSignal(false)
|
|
338
|
+
const msg = () => props.step.message
|
|
339
|
+
|
|
340
|
+
const parsedContent = () => {
|
|
341
|
+
const text = msg().content || ''
|
|
342
|
+
return tryParseJson(text) || text
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<>
|
|
347
|
+
<div
|
|
348
|
+
class={s().step}
|
|
349
|
+
onClick={() => setIsOpen(!isOpen())}
|
|
350
|
+
style={{ cursor: 'pointer' }}
|
|
351
|
+
>
|
|
352
|
+
<span class={`${s().stepPrefix} ${s().stepPrefixToolResult}`}>
|
|
353
|
+
Result
|
|
354
|
+
</span>
|
|
355
|
+
<span class={`${s().chevron} ${isOpen() ? s().chevronOpen : ''}`}>
|
|
356
|
+
{'\u25B6'}
|
|
357
|
+
</span>
|
|
358
|
+
</div>
|
|
359
|
+
<div class={`${s().cardBody} ${isOpen() ? s().cardBodyOpen : ''}`}>
|
|
360
|
+
<div class={s().cardBodyInner}>
|
|
361
|
+
<div class={s().stepJsonPanel}>
|
|
362
|
+
<JsonTree
|
|
363
|
+
value={parsedContent() as Record<string, unknown>}
|
|
364
|
+
defaultExpansionDepth={0}
|
|
365
|
+
copyable
|
|
366
|
+
/>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// --- Main component ---
|
|
375
|
+
|
|
376
|
+
export const IterationCard: Component<IterationCardProps> = (props) => {
|
|
377
|
+
const styles = useStyles()
|
|
378
|
+
const s = () => styles().iterationTimeline
|
|
379
|
+
const [isOpen, setIsOpen] = createSignal(props.isLast)
|
|
380
|
+
const [configExpanded, setConfigExpanded] = createSignal(false)
|
|
381
|
+
|
|
382
|
+
const iter = () => props.iteration
|
|
383
|
+
const isActive = () => !iter().completedAt
|
|
384
|
+
const isCompleted = () =>
|
|
385
|
+
!!iter().completedAt && iter().finishReason !== 'error'
|
|
386
|
+
const isError = () => iter().finishReason === 'error'
|
|
387
|
+
|
|
388
|
+
const duration = () => {
|
|
389
|
+
if (!iter().completedAt) return undefined
|
|
390
|
+
return iter().completedAt! - iter().startedAt
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const label = () => getIterationLabel(iter(), props.index)
|
|
394
|
+
const steps = createMemo(() => buildSteps(iter(), props.messages))
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Compute delta usage for display.
|
|
398
|
+
* The store holds cumulative usage per iteration (as reported by the provider).
|
|
399
|
+
* If a previous iteration exists on the same request, subtract its cumulative
|
|
400
|
+
* to get this iteration's incremental usage.
|
|
401
|
+
*/
|
|
402
|
+
const deltaUsage = createMemo(() => {
|
|
403
|
+
const usage = iter().usage
|
|
404
|
+
if (!usage) return undefined
|
|
405
|
+
const prev = props.previousIteration
|
|
406
|
+
if (!prev?.usage) return usage
|
|
407
|
+
// Only subtract if same request (cumulative values are per-request)
|
|
408
|
+
if (prev.requestId !== iter().requestId) return usage
|
|
409
|
+
return {
|
|
410
|
+
promptTokens: Math.max(0, usage.promptTokens - prev.usage.promptTokens),
|
|
411
|
+
completionTokens: Math.max(
|
|
412
|
+
0,
|
|
413
|
+
usage.completionTokens - prev.usage.completionTokens,
|
|
414
|
+
),
|
|
415
|
+
totalTokens: Math.max(0, usage.totalTokens - prev.usage.totalTokens),
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const finishLabel = () => {
|
|
420
|
+
if (!iter().finishReason) return null
|
|
421
|
+
switch (iter().finishReason) {
|
|
422
|
+
case 'stop':
|
|
423
|
+
return 'completed'
|
|
424
|
+
case 'tool_calls':
|
|
425
|
+
return 'tool calls'
|
|
426
|
+
case 'error':
|
|
427
|
+
return 'error'
|
|
428
|
+
case 'length':
|
|
429
|
+
return 'max length'
|
|
430
|
+
default:
|
|
431
|
+
return iter().finishReason
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const headerAccent = () => {
|
|
436
|
+
if (isActive()) return s().iterHeaderActive
|
|
437
|
+
if (isError()) return s().iterHeaderError
|
|
438
|
+
if (isCompleted()) return s().iterHeaderCompleted
|
|
439
|
+
return ''
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Config data from this iteration
|
|
443
|
+
const configSubtitle = () => {
|
|
444
|
+
const parts: Array<string> = []
|
|
445
|
+
if (iter().model) parts.push(iter().model!)
|
|
446
|
+
if (iter().provider) parts.push(iter().provider!)
|
|
447
|
+
return parts.length > 0 ? parts.join(' \u00B7 ') : null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const toolNames = () => iter().toolNames || []
|
|
451
|
+
const systemPrompts = () => iter().systemPrompts || []
|
|
452
|
+
|
|
453
|
+
/** Count actual tool invocations in this iteration's messages */
|
|
454
|
+
const toolInvocationCounts = createMemo(() => {
|
|
455
|
+
const counts = new Map<string, number>()
|
|
456
|
+
const msgIds = new Set(iter().messageIds)
|
|
457
|
+
for (const msg of props.messages) {
|
|
458
|
+
if (msgIds.has(msg.id) && msg.toolCalls) {
|
|
459
|
+
for (const tc of msg.toolCalls) {
|
|
460
|
+
counts.set(tc.name, (counts.get(tc.name) || 0) + 1)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return counts
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
const totalToolCalls = createMemo(() => {
|
|
468
|
+
let count = 0
|
|
469
|
+
for (const v of toolInvocationCounts().values()) count += v
|
|
470
|
+
return count
|
|
471
|
+
})
|
|
472
|
+
const modelOptions = () => iter().modelOptions
|
|
473
|
+
const hasModelOptions = () => {
|
|
474
|
+
const opts = modelOptions()
|
|
475
|
+
return opts && Object.keys(opts).length > 0
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const middlewareTransformCount = createMemo(() => {
|
|
479
|
+
let count = 0
|
|
480
|
+
for (const ev of iter().middlewareEvents) {
|
|
481
|
+
if (ev.hasTransform) count++
|
|
482
|
+
}
|
|
483
|
+
return count
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
const hasConfigChanged = () => {
|
|
487
|
+
const prev = props.previousIteration
|
|
488
|
+
if (!prev) return false
|
|
489
|
+
return (
|
|
490
|
+
iter().model !== prev.model ||
|
|
491
|
+
iter().provider !== prev.provider ||
|
|
492
|
+
JSON.stringify(iter().toolNames) !== JSON.stringify(prev.toolNames)
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const configDiffs = () => {
|
|
497
|
+
const prev = props.previousIteration
|
|
498
|
+
if (!prev) return []
|
|
499
|
+
const diffs: Array<{ key: string; from: string; to: string }> = []
|
|
500
|
+
if (iter().model !== prev.model) {
|
|
501
|
+
diffs.push({
|
|
502
|
+
key: 'model',
|
|
503
|
+
from: prev.model || '(none)',
|
|
504
|
+
to: iter().model || '(none)',
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
if (iter().provider !== prev.provider) {
|
|
508
|
+
diffs.push({
|
|
509
|
+
key: 'provider',
|
|
510
|
+
from: prev.provider || '(none)',
|
|
511
|
+
to: iter().provider || '(none)',
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
if (JSON.stringify(iter().toolNames) !== JSON.stringify(prev.toolNames)) {
|
|
515
|
+
diffs.push({
|
|
516
|
+
key: 'tools',
|
|
517
|
+
from: prev.toolNames?.join(', ') || '(none)',
|
|
518
|
+
to: iter().toolNames?.join(', ') || '(none)',
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
return diffs
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const hasExpandableConfig = () =>
|
|
525
|
+
toolNames().length > 0 || systemPrompts().length > 0 || hasModelOptions()
|
|
526
|
+
|
|
527
|
+
return (
|
|
528
|
+
<div
|
|
529
|
+
class={s().iterCard}
|
|
530
|
+
style={{ 'animation-delay': `${props.index * 60}ms` }}
|
|
531
|
+
>
|
|
532
|
+
{/* Header */}
|
|
533
|
+
<div
|
|
534
|
+
class={`${s().iterCardHeader} ${headerAccent()}`}
|
|
535
|
+
onClick={() => setIsOpen(!isOpen())}
|
|
536
|
+
>
|
|
537
|
+
<div class={s().cardHeaderContent}>
|
|
538
|
+
<span class={s().iterCardTitle}>{label()}</span>
|
|
539
|
+
{/* Config subtitle — same pattern as user message card */}
|
|
540
|
+
<div class={s().cardSubtitle}>
|
|
541
|
+
<Show when={configSubtitle()}>
|
|
542
|
+
<span class={s().subtitleText}>{configSubtitle()}</span>
|
|
543
|
+
</Show>
|
|
544
|
+
<Show when={toolNames().length > 0}>
|
|
545
|
+
<span class={s().subtitleBadge}>
|
|
546
|
+
{toolNames().length} tool{toolNames().length === 1 ? '' : 's'}
|
|
547
|
+
</span>
|
|
548
|
+
</Show>
|
|
549
|
+
<Show when={systemPrompts().length > 0}>
|
|
550
|
+
<span class={s().subtitleBadge}>
|
|
551
|
+
{systemPrompts().length} system prompt
|
|
552
|
+
{systemPrompts().length === 1 ? '' : 's'}
|
|
553
|
+
</span>
|
|
554
|
+
</Show>
|
|
555
|
+
<Show when={hasModelOptions()}>
|
|
556
|
+
<span class={s().subtitleBadge}>options</span>
|
|
557
|
+
</Show>
|
|
558
|
+
<Show when={hasConfigChanged()}>
|
|
559
|
+
<span class={s().subtitleBadgeWarn}>config changed</span>
|
|
560
|
+
</Show>
|
|
561
|
+
<Show when={middlewareTransformCount() > 0}>
|
|
562
|
+
<span class={s().subtitleBadgeWarn}>
|
|
563
|
+
{middlewareTransformCount()} middleware transform
|
|
564
|
+
{middlewareTransformCount() === 1 ? '' : 's'}
|
|
565
|
+
</span>
|
|
566
|
+
</Show>
|
|
567
|
+
<Show when={hasExpandableConfig()}>
|
|
568
|
+
<span
|
|
569
|
+
class={s().subtitleExpandToggle}
|
|
570
|
+
onClick={(e) => {
|
|
571
|
+
e.stopPropagation()
|
|
572
|
+
setConfigExpanded(!configExpanded())
|
|
573
|
+
}}
|
|
574
|
+
>
|
|
575
|
+
{configExpanded() ? 'hide config' : 'show config'}
|
|
576
|
+
</span>
|
|
577
|
+
</Show>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
<div class={s().cardHeaderBadges}>
|
|
581
|
+
<Show when={finishLabel()}>
|
|
582
|
+
<span
|
|
583
|
+
class={`${s().badge} ${
|
|
584
|
+
iter().finishReason === 'stop'
|
|
585
|
+
? s().badgeFinishReasonStop
|
|
586
|
+
: iter().finishReason === 'tool_calls'
|
|
587
|
+
? s().badgeFinishReasonToolCalls
|
|
588
|
+
: iter().finishReason === 'error'
|
|
589
|
+
? s().mwBadgeError
|
|
590
|
+
: s().badgeDuration
|
|
591
|
+
}`}
|
|
592
|
+
>
|
|
593
|
+
{finishLabel()}
|
|
594
|
+
</span>
|
|
595
|
+
</Show>
|
|
596
|
+
<Show when={totalToolCalls() > 0}>
|
|
597
|
+
<span
|
|
598
|
+
class={`${s().badge} ${s().badgeFinishReasonToolCalls}`}
|
|
599
|
+
title={`${totalToolCalls()} tool ${totalToolCalls() === 1 ? 'call' : 'calls'}`}
|
|
600
|
+
>
|
|
601
|
+
🔧 {totalToolCalls()}
|
|
602
|
+
</span>
|
|
603
|
+
</Show>
|
|
604
|
+
<Show when={duration()}>
|
|
605
|
+
<span
|
|
606
|
+
class={`${s().badge} ${s().badgeDuration}`}
|
|
607
|
+
title={formatDuration(duration())}
|
|
608
|
+
>
|
|
609
|
+
⏱️ {formatDuration(duration())}
|
|
610
|
+
</span>
|
|
611
|
+
</Show>
|
|
612
|
+
<Show when={deltaUsage()}>
|
|
613
|
+
<span
|
|
614
|
+
class={`${s().badge} ${s().badgeUsage}`}
|
|
615
|
+
title={`Prompt: ${deltaUsage()!.promptTokens.toLocaleString()} | Completion: ${deltaUsage()!.completionTokens.toLocaleString()}`}
|
|
616
|
+
>
|
|
617
|
+
🎯 {deltaUsage()!.totalTokens.toLocaleString()}
|
|
618
|
+
</span>
|
|
619
|
+
</Show>
|
|
620
|
+
<Show when={isActive()}>
|
|
621
|
+
<span class={`${s().badge} ${s().badgeDuration}`}>⟳ streaming</span>
|
|
622
|
+
</Show>
|
|
623
|
+
</div>
|
|
624
|
+
<span class={`${s().chevron} ${isOpen() ? s().chevronOpen : ''}`}>
|
|
625
|
+
{'\u25B6'}
|
|
626
|
+
</span>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
{/* Expandable config panel — between header and steps */}
|
|
630
|
+
<div
|
|
631
|
+
class={`${s().configPanelWrapper} ${configExpanded() ? s().configPanelWrapperOpen : ''}`}
|
|
632
|
+
>
|
|
633
|
+
<div class={s().configPanel}>
|
|
634
|
+
<div>
|
|
635
|
+
<Show when={hasConfigChanged()}>
|
|
636
|
+
<div class={s().configPanelSection}>
|
|
637
|
+
<span class={s().configPanelLabel}>Config Changes</span>
|
|
638
|
+
<div class={s().configDiffSection}>
|
|
639
|
+
<For each={configDiffs()}>
|
|
640
|
+
{(diff) => (
|
|
641
|
+
<div class={s().configDiffRow}>
|
|
642
|
+
<span class={s().configDiffKey}>{diff.key}:</span>
|
|
643
|
+
<span class={s().configDiffFrom}>{diff.from}</span>
|
|
644
|
+
<span class={s().configDiffArrow}>{'\u2192'}</span>
|
|
645
|
+
<span class={s().configDiffTo}>{diff.to}</span>
|
|
646
|
+
</div>
|
|
647
|
+
)}
|
|
648
|
+
</For>
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
</Show>
|
|
652
|
+
<Show when={toolNames().length > 0}>
|
|
653
|
+
<div class={s().configPanelSection}>
|
|
654
|
+
<span class={s().configPanelLabel}>Tools</span>
|
|
655
|
+
<div class={s().configToolsList}>
|
|
656
|
+
<For each={toolNames()}>
|
|
657
|
+
{(name) => (
|
|
658
|
+
<span class={s().configToolChip}>
|
|
659
|
+
{name}
|
|
660
|
+
<span class={s().configToolChipCount}>
|
|
661
|
+
{toolInvocationCounts().get(name) || 0}
|
|
662
|
+
</span>
|
|
663
|
+
</span>
|
|
664
|
+
)}
|
|
665
|
+
</For>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
</Show>
|
|
669
|
+
<Show when={systemPrompts().length > 0}>
|
|
670
|
+
<div class={s().configPanelSection}>
|
|
671
|
+
<span class={s().configPanelLabel}>
|
|
672
|
+
System Prompts ({systemPrompts().length})
|
|
673
|
+
</span>
|
|
674
|
+
<For each={systemPrompts()}>
|
|
675
|
+
{(prompt, i) => (
|
|
676
|
+
<SystemPromptItem prompt={prompt} index={i()} />
|
|
677
|
+
)}
|
|
678
|
+
</For>
|
|
679
|
+
</div>
|
|
680
|
+
</Show>
|
|
681
|
+
<Show when={hasModelOptions()}>
|
|
682
|
+
<div class={s().configPanelSection}>
|
|
683
|
+
<span class={s().configPanelLabel}>Model Options</span>
|
|
684
|
+
<div class={s().configJsonTreeContainer}>
|
|
685
|
+
<JsonTree
|
|
686
|
+
value={modelOptions() as Record<string, unknown>}
|
|
687
|
+
defaultExpansionDepth={2}
|
|
688
|
+
copyable
|
|
689
|
+
/>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</Show>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
|
|
697
|
+
{/* Body — step-by-step timeline */}
|
|
698
|
+
<div class={`${s().cardBody} ${isOpen() ? s().cardBodyOpen : ''}`}>
|
|
699
|
+
<div class={s().cardBodyInner}>
|
|
700
|
+
<Index each={steps()}>
|
|
701
|
+
{(step) => (
|
|
702
|
+
<Switch>
|
|
703
|
+
<Match when={step().kind === 'middleware'}>
|
|
704
|
+
<MiddlewareStep
|
|
705
|
+
step={
|
|
706
|
+
step() as Extract<IterationStep, { kind: 'middleware' }>
|
|
707
|
+
}
|
|
708
|
+
/>
|
|
709
|
+
</Match>
|
|
710
|
+
<Match when={step().kind === 'thinking'}>
|
|
711
|
+
<ThinkingStep
|
|
712
|
+
step={
|
|
713
|
+
step() as Extract<IterationStep, { kind: 'thinking' }>
|
|
714
|
+
}
|
|
715
|
+
/>
|
|
716
|
+
</Match>
|
|
717
|
+
<Match when={step().kind === 'assistant'}>
|
|
718
|
+
<AssistantStep
|
|
719
|
+
step={
|
|
720
|
+
step() as Extract<IterationStep, { kind: 'assistant' }>
|
|
721
|
+
}
|
|
722
|
+
/>
|
|
723
|
+
</Match>
|
|
724
|
+
<Match when={step().kind === 'tool_call'}>
|
|
725
|
+
<ToolCallStep
|
|
726
|
+
step={
|
|
727
|
+
step() as Extract<IterationStep, { kind: 'tool_call' }>
|
|
728
|
+
}
|
|
729
|
+
/>
|
|
730
|
+
</Match>
|
|
731
|
+
<Match when={step().kind === 'tool_result'}>
|
|
732
|
+
<ToolResultStep
|
|
733
|
+
step={
|
|
734
|
+
step() as Extract<IterationStep, { kind: 'tool_result' }>
|
|
735
|
+
}
|
|
736
|
+
/>
|
|
737
|
+
</Match>
|
|
738
|
+
</Switch>
|
|
739
|
+
)}
|
|
740
|
+
</Index>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
</div>
|
|
744
|
+
)
|
|
745
|
+
}
|