@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
@@ -0,0 +1,347 @@
1
+ import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
2
+ import { useAIStore } from '../../store/ai-context'
3
+ import { useStyles } from '../../styles/use-styles'
4
+ import { FixtureNamePopover } from './FixtureNamePopover'
5
+ import type {
6
+ HookRecord,
7
+ RegisteredTool,
8
+ ToolFixtureRecord,
9
+ } from '../../store/hook-registry'
10
+ import type { Component } from 'solid-js'
11
+
12
+ interface ToolFixtureFormProps {
13
+ hook: HookRecord
14
+ tool: RegisteredTool
15
+ onFire: (fixture: ToolFixtureRecord) => void
16
+ }
17
+
18
+ interface SchemaField {
19
+ name: string
20
+ type: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array'
21
+ required: boolean
22
+ description?: string
23
+ }
24
+
25
+ export const ToolFixtureForm: Component<ToolFixtureFormProps> = (props) => {
26
+ const { saveToolFixture } = useAIStore()
27
+ const styles = useStyles()
28
+ const fields = createMemo(() => fieldsFromSchema(props.tool.inputSchema))
29
+ const [inputValues, setInputValues] = createSignal<Record<string, string>>(
30
+ defaultValues(fields()),
31
+ )
32
+ const [rawInput, setRawInput] = createSignal('{}')
33
+ const [output, setOutput] = createSignal('null')
34
+ const [error, setError] = createSignal<string | null>(null)
35
+ const [pendingSaveName, setPendingSaveName] = createSignal<string | null>(
36
+ null,
37
+ )
38
+
39
+ createEffect(() => {
40
+ void props.tool.name
41
+ setInputValues(defaultValues(fields()))
42
+ setRawInput('{}')
43
+ setOutput('null')
44
+ setError(null)
45
+ setPendingSaveName(null)
46
+ })
47
+
48
+ const setFieldValue = (name: string, value: string) => {
49
+ setInputValues((current) => ({ ...current, [name]: value }))
50
+ }
51
+
52
+ const createFixture = (name?: string): ToolFixtureRecord | undefined => {
53
+ try {
54
+ setError(null)
55
+ const createdAt = Date.now()
56
+ const parsedInput =
57
+ fields().length > 0
58
+ ? parseObjectInput(fields(), inputValues())
59
+ : parseJson(rawInput(), 'Input')
60
+ const parsedOutput = parseJson(output(), 'Output')
61
+ return {
62
+ id: `fixture:${props.hook.id}:${props.tool.name}:${createdAt}:${Math.random()
63
+ .toString(36)
64
+ .slice(2)}`,
65
+ createdAt,
66
+ ...(name ? { name } : {}),
67
+ hookId: props.hook.id,
68
+ ...(props.hook.threadId ? { threadId: props.hook.threadId } : {}),
69
+ toolName: props.tool.name,
70
+ input: parsedInput,
71
+ output: parsedOutput,
72
+ execute: true,
73
+ }
74
+ } catch (fixtureError) {
75
+ setError(
76
+ fixtureError instanceof Error
77
+ ? fixtureError.message
78
+ : 'Unable to create fixture.',
79
+ )
80
+ return undefined
81
+ }
82
+ }
83
+
84
+ const handleFire = () => {
85
+ const fixture = createFixture()
86
+ if (!fixture) return
87
+ props.onFire(fixture)
88
+ }
89
+
90
+ const handleSave = () => {
91
+ setPendingSaveName(`${props.tool.name} fixture`)
92
+ }
93
+
94
+ const handleConfirmSave = (name: string) => {
95
+ const fixture = createFixture(name)
96
+ if (!fixture) return
97
+ saveToolFixture(fixture)
98
+ setPendingSaveName(null)
99
+ }
100
+
101
+ return (
102
+ <div
103
+ class={styles().hookDetails.fixtureForm}
104
+ data-testid="ai-devtools-tool-fixture-form"
105
+ data-tool-name={props.tool.name}
106
+ >
107
+ <div class={styles().hookDetails.sectionTitle}>Tool Fixture</div>
108
+ <div class={styles().hookDetails.fixtureHelp}>
109
+ {props.tool.description || props.tool.name}
110
+ </div>
111
+
112
+ <Show
113
+ when={fields().length > 0}
114
+ fallback={
115
+ <label class={styles().hookDetails.fixtureField}>
116
+ <span class={styles().hookDetails.fixtureLabel}>Input JSON</span>
117
+ <textarea
118
+ class={styles().hookDetails.fixtureTextarea}
119
+ value={rawInput()}
120
+ onInput={(event) => setRawInput(event.currentTarget.value)}
121
+ spellcheck={false}
122
+ />
123
+ </label>
124
+ }
125
+ >
126
+ <For each={fields()}>
127
+ {(field) => (
128
+ <label class={styles().hookDetails.fixtureField}>
129
+ <span class={styles().hookDetails.fixtureLabel}>
130
+ {field.name}
131
+ <Show when={field.required}>
132
+ <span class={styles().hookDetails.requiredMark}>*</span>
133
+ </Show>
134
+ </span>
135
+ <Show when={field.description}>
136
+ <span class={styles().hookDetails.fixtureHelp}>
137
+ {field.description}
138
+ </span>
139
+ </Show>
140
+ <FieldInput
141
+ field={field}
142
+ value={inputValues()[field.name] ?? ''}
143
+ onInput={(value) => setFieldValue(field.name, value)}
144
+ />
145
+ </label>
146
+ )}
147
+ </For>
148
+ </Show>
149
+
150
+ <label class={styles().hookDetails.fixtureField}>
151
+ <span class={styles().hookDetails.fixtureLabel}>Output JSON</span>
152
+ <textarea
153
+ class={styles().hookDetails.fixtureTextarea}
154
+ value={output()}
155
+ onInput={(event) => setOutput(event.currentTarget.value)}
156
+ spellcheck={false}
157
+ />
158
+ </label>
159
+
160
+ <Show when={error()}>
161
+ {(message) => (
162
+ <div class={styles().hookDetails.fixtureError}>{message()}</div>
163
+ )}
164
+ </Show>
165
+
166
+ <div class={styles().hookDetails.fixtureActions}>
167
+ <button
168
+ class={styles().hookDetails.fixtureButton}
169
+ data-testid="ai-devtools-tool-fire"
170
+ type="button"
171
+ onClick={handleFire}
172
+ >
173
+ Fire
174
+ </button>
175
+ <button
176
+ class={`${styles().hookDetails.fixtureButton} ${styles().hookDetails.fixtureButtonSecondary}`}
177
+ data-testid="ai-devtools-tool-save"
178
+ type="button"
179
+ onClick={handleSave}
180
+ >
181
+ Save
182
+ </button>
183
+ </div>
184
+ <Show when={pendingSaveName()}>
185
+ {(defaultName) => (
186
+ <FixtureNamePopover
187
+ defaultName={defaultName()}
188
+ onCancel={() => setPendingSaveName(null)}
189
+ onSave={handleConfirmSave}
190
+ />
191
+ )}
192
+ </Show>
193
+ </div>
194
+ )
195
+ }
196
+
197
+ const FieldInput: Component<{
198
+ field: SchemaField
199
+ value: string
200
+ onInput: (value: string) => void
201
+ }> = (props) => {
202
+ const styles = useStyles()
203
+
204
+ if (props.field.type === 'boolean') {
205
+ return (
206
+ <select
207
+ class={styles().hookDetails.fixtureInput}
208
+ value={props.value}
209
+ onInput={(event) => props.onInput(event.currentTarget.value)}
210
+ >
211
+ <option value="false">false</option>
212
+ <option value="true">true</option>
213
+ </select>
214
+ )
215
+ }
216
+
217
+ if (props.field.type === 'object' || props.field.type === 'array') {
218
+ return (
219
+ <textarea
220
+ class={styles().hookDetails.fixtureTextarea}
221
+ value={props.value}
222
+ onInput={(event) => props.onInput(event.currentTarget.value)}
223
+ spellcheck={false}
224
+ />
225
+ )
226
+ }
227
+
228
+ return (
229
+ <input
230
+ class={styles().hookDetails.fixtureInput}
231
+ type={props.field.type === 'string' ? 'text' : 'number'}
232
+ value={props.value}
233
+ onInput={(event) => props.onInput(event.currentTarget.value)}
234
+ />
235
+ )
236
+ }
237
+
238
+ function fieldsFromSchema(schema: unknown): Array<SchemaField> {
239
+ if (!isJsonSchemaObject(schema)) return []
240
+ const properties = schema.properties
241
+ if (!isRecord(properties)) return []
242
+ const required = Array.isArray(schema.required)
243
+ ? new Set(
244
+ schema.required.filter(
245
+ (item): item is string => typeof item === 'string',
246
+ ),
247
+ )
248
+ : new Set<string>()
249
+
250
+ return Object.entries(properties).map(([name, property]) => {
251
+ const propertySchema = isJsonSchemaObject(property) ? property : undefined
252
+ const type = normalizeType(propertySchema?.type)
253
+ return {
254
+ name,
255
+ type,
256
+ required: required.has(name),
257
+ ...(typeof propertySchema?.description === 'string'
258
+ ? { description: propertySchema.description }
259
+ : {}),
260
+ }
261
+ })
262
+ }
263
+
264
+ function defaultValues(fields: Array<SchemaField>): Record<string, string> {
265
+ const values: Record<string, string> = {}
266
+ for (const field of fields) {
267
+ if (field.type === 'boolean') {
268
+ values[field.name] = 'false'
269
+ } else if (field.type === 'number' || field.type === 'integer') {
270
+ values[field.name] = '0'
271
+ } else if (field.type === 'array') {
272
+ values[field.name] = '[]'
273
+ } else if (field.type === 'object') {
274
+ values[field.name] = '{}'
275
+ } else {
276
+ values[field.name] = ''
277
+ }
278
+ }
279
+ return values
280
+ }
281
+
282
+ function parseObjectInput(
283
+ fields: Array<SchemaField>,
284
+ values: Record<string, string>,
285
+ ): Record<string, unknown> {
286
+ const input: Record<string, unknown> = {}
287
+ for (const field of fields) {
288
+ const rawValue = values[field.name] ?? ''
289
+ if (!field.required && rawValue.length === 0) {
290
+ continue
291
+ }
292
+ input[field.name] = parseFieldValue(field, rawValue)
293
+ }
294
+ return input
295
+ }
296
+
297
+ function parseFieldValue(field: SchemaField, rawValue: string): unknown {
298
+ if (field.type === 'boolean') return rawValue === 'true'
299
+ if (field.type === 'number') {
300
+ const value = Number(rawValue)
301
+ if (!Number.isFinite(value)) {
302
+ throw new Error(`${field.name} must be a number.`)
303
+ }
304
+ return value
305
+ }
306
+ if (field.type === 'integer') {
307
+ const value = Number(rawValue)
308
+ if (!Number.isInteger(value)) {
309
+ throw new Error(`${field.name} must be an integer.`)
310
+ }
311
+ return value
312
+ }
313
+ if (field.type === 'object' || field.type === 'array') {
314
+ return parseJson(rawValue, field.name)
315
+ }
316
+ return rawValue
317
+ }
318
+
319
+ function parseJson(value: string, label: string): unknown {
320
+ try {
321
+ return JSON.parse(value)
322
+ } catch {
323
+ throw new Error(`${label} must be valid JSON.`)
324
+ }
325
+ }
326
+
327
+ function normalizeType(value: unknown): SchemaField['type'] {
328
+ if (value === 'boolean') return 'boolean'
329
+ if (value === 'number') return 'number'
330
+ if (value === 'integer') return 'integer'
331
+ if (value === 'array') return 'array'
332
+ if (value === 'object') return 'object'
333
+ return 'string'
334
+ }
335
+
336
+ function isJsonSchemaObject(value: unknown): value is {
337
+ type?: unknown
338
+ description?: unknown
339
+ properties?: unknown
340
+ required?: unknown
341
+ } {
342
+ return isRecord(value)
343
+ }
344
+
345
+ function isRecord(value: unknown): value is Record<string, unknown> {
346
+ return typeof value === 'object' && value !== null
347
+ }
@@ -0,0 +1,134 @@
1
+ import type { HookRecord, RunRecord } from '../../store/hook-registry'
2
+
3
+ export type HookCategoryId =
4
+ | 'chat'
5
+ | 'structured'
6
+ | 'image'
7
+ | 'audio'
8
+ | 'video'
9
+ | 'speech'
10
+ | 'transcription'
11
+ | 'summarize'
12
+ | 'text'
13
+ | 'other'
14
+
15
+ export interface HookCategoryGroup {
16
+ id: HookCategoryId
17
+ label: string
18
+ hooks: Array<HookRecord>
19
+ }
20
+
21
+ export interface HookDashboardSummary {
22
+ total: number
23
+ active: number
24
+ running: number
25
+ categories: number
26
+ tools: number
27
+ runs: number
28
+ }
29
+
30
+ const hookCategoryOrder: Array<HookCategoryId> = [
31
+ 'chat',
32
+ 'structured',
33
+ 'image',
34
+ 'audio',
35
+ 'video',
36
+ 'speech',
37
+ 'transcription',
38
+ 'summarize',
39
+ 'text',
40
+ 'other',
41
+ ]
42
+
43
+ const hookCategoryLabels: Record<HookCategoryId, string> = {
44
+ chat: 'Chat',
45
+ structured: 'Structured',
46
+ image: 'Image',
47
+ audio: 'Audio',
48
+ video: 'Video',
49
+ speech: 'Speech',
50
+ transcription: 'Transcription',
51
+ summarize: 'Summarize',
52
+ text: 'Text',
53
+ other: 'Other',
54
+ }
55
+
56
+ export function visibleHooks(hooks: Array<HookRecord>): Array<HookRecord> {
57
+ return hooks
58
+ }
59
+
60
+ export function getHookDisplayName(hook: HookRecord): string {
61
+ return hook.displayName ?? hook.hookName
62
+ }
63
+
64
+ export function groupHooksByCategory(
65
+ hooks: Array<HookRecord>,
66
+ ): Array<HookCategoryGroup> {
67
+ const groups = new Map<HookCategoryId, Array<HookRecord>>()
68
+
69
+ for (const hook of hooks) {
70
+ const category = inferHookCategory(hook)
71
+ groups.set(category, [...(groups.get(category) ?? []), hook])
72
+ }
73
+
74
+ return hookCategoryOrder.flatMap((id) => {
75
+ const categoryHooks = groups.get(id)
76
+ if (!categoryHooks?.length) return []
77
+
78
+ return [
79
+ {
80
+ id,
81
+ label: hookCategoryLabels[id],
82
+ hooks: [...categoryHooks].sort(
83
+ (a, b) => a.registeredAt - b.registeredAt,
84
+ ),
85
+ },
86
+ ]
87
+ })
88
+ }
89
+
90
+ export function createHookDashboardSummary(
91
+ hooks: Array<HookRecord>,
92
+ runs: Record<string, RunRecord>,
93
+ ): HookDashboardSummary {
94
+ return {
95
+ total: hooks.length,
96
+ active: hooks.length,
97
+ running: hooks.filter((hook) => isHookRunning(hook, runs)).length,
98
+ categories: groupHooksByCategory(hooks).length,
99
+ tools: hooks.reduce((count, hook) => count + hook.tools.length, 0),
100
+ runs: Object.keys(runs).length,
101
+ }
102
+ }
103
+
104
+ export function isHookRunning(
105
+ hook: HookRecord,
106
+ runs: Record<string, RunRecord>,
107
+ ): boolean {
108
+ if (hook.lifecycle === 'streaming') {
109
+ return true
110
+ }
111
+
112
+ return hook.runIds.some((runId) => {
113
+ const run = runs[runId]
114
+ return run ? isRunActive(run) : false
115
+ })
116
+ }
117
+
118
+ function isRunActive(run: RunRecord): boolean {
119
+ return (
120
+ run.status === 'created' ||
121
+ run.status === 'started' ||
122
+ run.status === 'updated'
123
+ )
124
+ }
125
+
126
+ function inferHookCategory(hook: HookRecord): HookCategoryId {
127
+ const hookName = hook.hookName.toLowerCase()
128
+
129
+ if (hookName.includes('speech')) return 'speech'
130
+ if (hookName.includes('transcription')) return 'transcription'
131
+ if (hookName.includes('summarize')) return 'summarize'
132
+
133
+ return hook.outputKind ?? 'other'
134
+ }
@@ -0,0 +1,4 @@
1
+ export { HookDashboard } from './HookDashboard'
2
+ export { HookDetails } from './HookDetails'
3
+ export { GenerationPanel, GenerationPreview } from './GenerationPanel'
4
+ export { ToolFixtureForm } from './ToolFixtureForm'
@@ -0,0 +1,117 @@
1
+ export interface PreviewMessageForMerge {
2
+ id: string
3
+ role?: string
4
+ content?: string
5
+ parts?: Array<PreviewPartForMerge>
6
+ }
7
+
8
+ export interface PreviewPartForMerge {
9
+ id: string
10
+ kind?: string
11
+ content?: string
12
+ }
13
+
14
+ export function mergePreviewMessagesForUserView<
15
+ TMessage extends PreviewMessageForMerge,
16
+ >(
17
+ conversationMessages: Array<TMessage>,
18
+ snapshotMessages: Array<TMessage>,
19
+ ): Array<TMessage> {
20
+ if (conversationMessages.length === 0) {
21
+ return snapshotMessages.filter(isRenderablePreviewMessage)
22
+ }
23
+
24
+ const conversationIds = new Set(
25
+ conversationMessages.map((message) => message.id),
26
+ )
27
+ const snapshotById = new Map(
28
+ snapshotMessages
29
+ .filter(isRenderablePreviewMessage)
30
+ .map((message) => [message.id, message] as const),
31
+ )
32
+
33
+ const renderableConversationMessages = conversationMessages.filter(
34
+ isRenderablePreviewMessage,
35
+ )
36
+ const shouldUseSnapshotNonUserMessages =
37
+ renderableConversationMessages.length > 0 &&
38
+ !renderableConversationMessages.some(isNonUserPreviewMessage)
39
+
40
+ const mergedMessages = [
41
+ ...conversationMessages.map(
42
+ (message) => snapshotById.get(message.id) ?? message,
43
+ ),
44
+ ...snapshotMessages.filter(
45
+ (message) =>
46
+ !conversationIds.has(message.id) &&
47
+ (isFixtureReplayMessage(message) ||
48
+ (shouldUseSnapshotNonUserMessages &&
49
+ isNonUserPreviewMessage(message))),
50
+ ),
51
+ ]
52
+
53
+ return mergedMessages.filter(isRenderablePreviewMessage)
54
+ }
55
+
56
+ export function partsToMessageText(parts: Array<unknown>): string {
57
+ return parts
58
+ .map((part) => {
59
+ if (!isRecord(part)) return ''
60
+ if (part.type !== 'text') return ''
61
+ if (typeof part.text === 'string') return part.text
62
+ if (typeof part.content === 'string') return part.content
63
+ return ''
64
+ })
65
+ .filter(Boolean)
66
+ .join('\n')
67
+ }
68
+
69
+ export function visiblePreviewPartsForMessage<
70
+ TPart extends PreviewPartForMerge,
71
+ >(message: { content?: string; parts?: Array<TPart> }): Array<TPart> {
72
+ const messageContent = normalizeText(message.content)
73
+ return (message.parts ?? []).filter((part) => {
74
+ if (part.kind !== 'text') return true
75
+ if (!messageContent) return true
76
+ return normalizeText(part.content) !== messageContent
77
+ })
78
+ }
79
+
80
+ export function hasStructuredOutputPreview(
81
+ messages: Array<PreviewMessageForMerge>,
82
+ ): boolean {
83
+ return messages.some((message) =>
84
+ message.parts?.some((part) => part.kind === 'structured-output'),
85
+ )
86
+ }
87
+
88
+ function isFixtureReplayMessage(message: PreviewMessageForMerge): boolean {
89
+ if (message.id.startsWith('fixture-msg-')) {
90
+ return true
91
+ }
92
+
93
+ return Boolean(
94
+ message.parts?.some((part) =>
95
+ part.id.startsWith('tool-call:fixture-tool-call-'),
96
+ ),
97
+ )
98
+ }
99
+
100
+ function isRenderablePreviewMessage(message: PreviewMessageForMerge): boolean {
101
+ if (message.content?.trim()) {
102
+ return true
103
+ }
104
+ return Boolean(message.parts?.length)
105
+ }
106
+
107
+ function isNonUserPreviewMessage(message: PreviewMessageForMerge): boolean {
108
+ return message.role !== undefined && message.role !== 'user'
109
+ }
110
+
111
+ function isRecord(value: unknown): value is Record<string, unknown> {
112
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value))
113
+ }
114
+
115
+ function normalizeText(value: string | undefined): string {
116
+ return value?.trim() ?? ''
117
+ }