@tanstack/ai-devtools-core 0.5.9 → 0.5.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.
@@ -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/WD22ACIB.js')
5
+ () => import('./Shell/JT2B6WKX.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/DRZSZBFE.js')
5
+ () => import('./Shell/YBXRSRRK.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/G2SFOZV2.js')
5
+ () => import('./Shell/DCUFLPG6.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/G2SFOZV2.js')
5
+ () => import('./Shell/DCUFLPG6.js')
6
6
  );
7
7
 
8
8
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-devtools-core",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "Core TanStack AI Devtools plugin for inspecting chat messages, tool calls, streams, and errors.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -84,8 +84,8 @@
84
84
  "@tanstack/devtools-utils": "^0.7.0",
85
85
  "goober": "^2.1.18",
86
86
  "solid-js": "^1.9.10",
87
- "@tanstack/ai": "^0.50.0",
88
- "@tanstack/ai-event-client": "^0.11.0"
87
+ "@tanstack/ai": "^0.52.0",
88
+ "@tanstack/ai-event-client": "^0.11.2"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@vitest/coverage-v8": "4.1.10",
@@ -169,6 +169,9 @@ const MiddlewareStep: Component<{
169
169
  if (ev().wasDropped) return 'DROP'
170
170
  if (ev().hookName === 'onChunk' && ev().hasTransform) return 'TRANSFORM'
171
171
  if (ev().hookName === 'onConfig' && ev().hasTransform) return 'TRANSFORM'
172
+ if (ev().hookName === 'onCompactStart') return 'START'
173
+ if (ev().hookName === 'onCompact') return 'COMPACT'
174
+ if (ev().hookName === 'onCompactEnd') return 'END'
172
175
  if (ev().hookName === 'onBeforeToolCall' && ev().hasTransform)
173
176
  return 'DECISION'
174
177
  return null
@@ -181,7 +184,13 @@ const MiddlewareStep: Component<{
181
184
 
182
185
  return (
183
186
  <>
184
- <div class={s().step}>
187
+ <div
188
+ class={s().step}
189
+ style={hasChanges() ? { cursor: 'pointer' } : undefined}
190
+ onClick={() => {
191
+ if (hasChanges()) setExpanded(!expanded())
192
+ }}
193
+ >
185
194
  <span class={`${s().stepPrefix} ${s().stepPrefixMiddleware}`}>
186
195
  Middleware
187
196
  </span>
@@ -196,10 +205,7 @@ const MiddlewareStep: Component<{
196
205
  <span class={s().mwSuffix}>{suffix()}</span>
197
206
  </Show>
198
207
  <Show when={hasChanges()}>
199
- <span
200
- class={s().stepExpandToggle}
201
- onClick={() => setExpanded(!expanded())}
202
- >
208
+ <span class={s().stepExpandToggle}>
203
209
  {expanded() ? 'hide changes' : 'show changes'}
204
210
  </span>
205
211
  </Show>
@@ -0,0 +1,223 @@
1
+ import { For, Show, createMemo, createSignal } from 'solid-js'
2
+ import { JsonTree } from '@tanstack/devtools-ui'
3
+ import { useAIStore } from '../../store/ai-context'
4
+ import { useStyles } from '../../styles/use-styles'
5
+ import { compactionEventsForHook } from '../../store/compaction-registry'
6
+ import type { Component } from 'solid-js'
7
+ import type {
8
+ CompactionEventRecord,
9
+ CompactionMessagePreview,
10
+ } from '../../store/compaction-registry'
11
+ import type { HookRecord } from '../../store/hook-registry'
12
+
13
+ function formatTime(value: number): string {
14
+ const date = new Date(value)
15
+ if (Number.isNaN(date.getTime())) return ''
16
+ return date.toLocaleTimeString()
17
+ }
18
+
19
+ function shortStrategy(event: CompactionEventRecord): string {
20
+ const key = event.strategyKey
21
+ if (!key) return event.reusedCheckpoint ? 'checkpoint' : 'compaction'
22
+ const cut = key.indexOf(':maxTokens=')
23
+ return cut === -1 ? key : key.slice(0, cut)
24
+ }
25
+
26
+ const PreviewMessage: Component<{
27
+ preview: CompactionMessagePreview
28
+ }> = (props) => {
29
+ const styles = useStyles()
30
+ return (
31
+ <div class={styles().hookDetails.message}>
32
+ <div class={styles().hookDetails.messageRole}>
33
+ {props.preview.role}
34
+ <Show when={props.preview.tokens > 0}>
35
+ {` · ${props.preview.tokens} tok`}
36
+ </Show>
37
+ </div>
38
+ <div class={styles().hookDetails.messageContent}>
39
+ {props.preview.text}
40
+ </div>
41
+ </div>
42
+ )
43
+ }
44
+
45
+ const PreviewColumn: Component<{
46
+ title: string
47
+ previews: Array<CompactionMessagePreview>
48
+ empty: string
49
+ }> = (props) => {
50
+ const styles = useStyles()
51
+ const s = () => styles().iterationTimeline
52
+ return (
53
+ <div class={s().stepJsonItem}>
54
+ <div class={s().stepJsonItemLabel}>
55
+ {props.title} ({props.previews.length})
56
+ </div>
57
+ <Show
58
+ when={props.previews.length > 0}
59
+ fallback={
60
+ <div class={styles().hookDetails.emptySmall}>{props.empty}</div>
61
+ }
62
+ >
63
+ <div class={styles().memoryPanel.list}>
64
+ <For each={props.previews}>
65
+ {(preview) => <PreviewMessage preview={preview} />}
66
+ </For>
67
+ </div>
68
+ </Show>
69
+ </div>
70
+ )
71
+ }
72
+
73
+ function kindLabel(kind: CompactionEventRecord['kind']): string {
74
+ if (kind === 'started') return 'Started'
75
+ if (kind === 'ended') return 'Ended'
76
+ return 'State'
77
+ }
78
+
79
+ const CompactEvent: Component<{
80
+ event: CompactionEventRecord
81
+ defaultOpen?: boolean
82
+ }> = (props) => {
83
+ const styles = useStyles()
84
+ const s = () => styles().iterationTimeline
85
+ const [expanded, setExpanded] = createSignal(props.defaultOpen === true)
86
+ const event = () => props.event
87
+ const isState = () => event().kind === 'state'
88
+
89
+ const stats = () => ({
90
+ kind: event().kind,
91
+ before: event().before,
92
+ after: event().after,
93
+ messagesBefore: event().messagesBefore,
94
+ messagesAfter: event().messagesAfter,
95
+ maxTokens: event().maxTokens,
96
+ strategyKey: event().strategyKey,
97
+ reusedCheckpoint: event().reusedCheckpoint,
98
+ durationMs: event().durationMs,
99
+ })
100
+
101
+ const countLabel = () => {
102
+ if (event().kind === 'started') {
103
+ return `${event().messagesBefore ?? 0} msgs · ${event().before ?? 0} tok`
104
+ }
105
+ if (event().kind === 'ended') {
106
+ return `${event().messagesAfter ?? 0} msgs · ${event().after ?? 0} tok`
107
+ }
108
+ return `${event().messagesBefore ?? 0} → ${event().messagesAfter ?? 0} msgs`
109
+ }
110
+
111
+ return (
112
+ <>
113
+ <div
114
+ class={s().step}
115
+ data-testid="ai-devtools-compaction-event"
116
+ data-kind={event().kind}
117
+ style={{ cursor: 'pointer' }}
118
+ onClick={() => setExpanded(!expanded())}
119
+ >
120
+ <span class={`${s().stepPrefix} ${s().stepPrefixMiddleware}`}>
121
+ {kindLabel(event().kind)}
122
+ </span>
123
+ <span class={`${s().mwBadge} ${s().mwBadgeTransform}`}>
124
+ {shortStrategy(event())}
125
+ </span>
126
+ <span class={s().mwHook}>{countLabel()}</span>
127
+ <Show when={event().kind === 'state'}>
128
+ <span class={s().stepDuration}>
129
+ {event().before} → {event().after} tok
130
+ </span>
131
+ </Show>
132
+ <Show when={event().durationMs !== undefined}>
133
+ <span class={s().stepDuration}>{event().durationMs}ms</span>
134
+ </Show>
135
+ <Show when={event().reusedCheckpoint}>
136
+ <span class={s().mwSuffix}>checkpoint</span>
137
+ </Show>
138
+ <span class={s().stepDuration}>{formatTime(event().timestamp)}</span>
139
+ <span class={`${s().chevron} ${expanded() ? s().chevronOpen : ''}`}>
140
+ {'\u25B6'}
141
+ </span>
142
+ </div>
143
+ <Show when={expanded()}>
144
+ <div class={s().mwChangesContainer}>
145
+ <JsonTree value={stats()} defaultExpansionDepth={1} copyable />
146
+ </div>
147
+ <Show when={isState()}>
148
+ <div class={s().stepJsonItemsCompare}>
149
+ <PreviewColumn
150
+ title="Dropped"
151
+ previews={event().dropped ?? []}
152
+ empty="Nothing was dropped."
153
+ />
154
+ <PreviewColumn
155
+ title="Sent to model"
156
+ previews={event().result ?? []}
157
+ empty="No compacted transcript on this event."
158
+ />
159
+ </div>
160
+ </Show>
161
+ </Show>
162
+ </>
163
+ )
164
+ }
165
+
166
+ export const CompactionPanel: Component<{ hook: HookRecord }> = (props) => {
167
+ const { state, clearCompaction } = useAIStore()
168
+ const styles = useStyles()
169
+ const s = () => styles().iterationTimeline
170
+
171
+ const events = createMemo(() =>
172
+ compactionEventsForHook(state.compaction, props.hook),
173
+ )
174
+ const lastStateIndex = createMemo(() => {
175
+ const list = events()
176
+ for (let i = list.length - 1; i >= 0; i--) {
177
+ if (list[i]?.kind === 'state') return i
178
+ }
179
+ return -1
180
+ })
181
+
182
+ return (
183
+ <div
184
+ class={styles().memoryPanel.container}
185
+ data-testid="ai-devtools-compaction-panel"
186
+ >
187
+ <Show
188
+ when={events().length > 0}
189
+ fallback={
190
+ <div class={styles().hookDetails.emptySmall}>
191
+ No compaction yet. When the transcript passes maxTokens, each
192
+ compact appears here.
193
+ </div>
194
+ }
195
+ >
196
+ <div class={styles().memoryPanel.toolbar}>
197
+ <span class={styles().memoryPanel.badge}>
198
+ {events().length} event{events().length === 1 ? '' : 's'}
199
+ </span>
200
+ <button
201
+ type="button"
202
+ class={styles().memoryPanel.clearButton}
203
+ onClick={() => clearCompaction()}
204
+ >
205
+ Clear
206
+ </button>
207
+ </div>
208
+ <div class={s().iterCard}>
209
+ <For each={events()}>
210
+ {(event, index) => (
211
+ <CompactEvent
212
+ event={event}
213
+ defaultOpen={
214
+ event.kind === 'state' && index() === lastStateIndex()
215
+ }
216
+ />
217
+ )}
218
+ </For>
219
+ </div>
220
+ </Show>
221
+ </div>
222
+ )
223
+ }
@@ -55,8 +55,9 @@ export const HookDashboard: Component = () => {
55
55
 
56
56
  const handleSelect = (hook: HookRecord) => {
57
57
  selectHook(hook.id)
58
- if (state.conversations[hook.id]) {
59
- selectConversation(hook.id)
58
+ const conversationId = hook.id || hook.clientId || hook.threadId
59
+ if (conversationId && state.conversations[conversationId]) {
60
+ selectConversation(conversationId)
60
61
  }
61
62
  }
62
63
 
@@ -39,6 +39,8 @@ import {
39
39
  } from './preview-messages'
40
40
  import { GenerationPanel, GenerationPreview } from './GenerationPanel'
41
41
  import { MemoryPanel } from './MemoryPanel'
42
+ import { CompactionPanel } from './CompactionPanel'
43
+ import { SkillsPanel } from './SkillsPanel'
42
44
  import type { HoverOrigin, HoverTarget, PreviewJsonItem } from './preview-model'
43
45
  import type {
44
46
  HookRecord,
@@ -49,7 +51,13 @@ import type {
49
51
  import type { Conversation, Message, ToolCall } from '../../store/ai-store'
50
52
  import type { Component, Setter } from 'solid-js'
51
53
 
52
- type DetailTab = 'conversation' | 'tools' | 'state' | 'memory'
54
+ type DetailTab =
55
+ | 'conversation'
56
+ | 'tools'
57
+ | 'state'
58
+ | 'memory'
59
+ | 'compaction'
60
+ | 'skills'
53
61
  type MessagePart = NonNullable<Message['parts']>[number]
54
62
  const scrollAnimations = new WeakMap<HTMLElement, number>()
55
63
 
@@ -119,7 +127,10 @@ export const HookDetails: Component = () => {
119
127
 
120
128
  const hook = createMemo((): HookRecord | undefined => {
121
129
  const id = state.hooks.activeHookId
122
- return id ? state.hooks.hooks[id] : undefined
130
+ // `''` is a legal registry key (a hook that mounted before it minted a
131
+ // thread id). Only `null` means "no selection".
132
+ if (id === null) return undefined
133
+ return state.hooks.hooks[id]
123
134
  })
124
135
 
125
136
  const conversation = createMemo(() => {
@@ -151,7 +162,10 @@ export const HookDetails: Component = () => {
151
162
  // while one of them is selected, fall back to the conversation view.
152
163
  if (
153
164
  isGenerationHook() &&
154
- (activeTab() === 'tools' || activeTab() === 'memory')
165
+ (activeTab() === 'tools' ||
166
+ activeTab() === 'memory' ||
167
+ activeTab() === 'compaction' ||
168
+ activeTab() === 'skills')
155
169
  ) {
156
170
  setActiveTab('conversation')
157
171
  }
@@ -175,7 +189,11 @@ export const HookDetails: Component = () => {
175
189
  // Tools tab owns its own form/saved-fixtures layout that fills the
176
190
  // primary pane; the secondary "User View" preview squeezes the tool
177
191
  // detail column to zero width on narrower hookDetails widths.
178
- if (activeTab() === 'tools' && !isGenerationHook()) return false
192
+ if (
193
+ (activeTab() === 'tools' || activeTab() === 'skills') &&
194
+ !isGenerationHook()
195
+ )
196
+ return false
179
197
  return isGenerationHook() || !hasStructuredOutputPreview(previewMessages())
180
198
  })
181
199
 
@@ -204,8 +222,10 @@ export const HookDetails: Component = () => {
204
222
  <HookOverview
205
223
  onSelectHook={(selectedHook) => {
206
224
  selectHook(selectedHook.id)
207
- if (state.conversations[selectedHook.id]) {
208
- selectConversation(selectedHook.id)
225
+ const conversationId =
226
+ selectedHook.id || selectedHook.clientId || selectedHook.threadId
227
+ if (conversationId && state.conversations[conversationId]) {
228
+ selectConversation(conversationId)
209
229
  }
210
230
  }}
211
231
  />
@@ -255,6 +275,18 @@ export const HookDetails: Component = () => {
255
275
  activeTab={activeTab()}
256
276
  onSelect={setActiveTab}
257
277
  />
278
+ <TabButton
279
+ label="Compaction"
280
+ tab="compaction"
281
+ activeTab={activeTab()}
282
+ onSelect={setActiveTab}
283
+ />
284
+ <TabButton
285
+ label="Skills"
286
+ tab="skills"
287
+ activeTab={activeTab()}
288
+ onSelect={setActiveTab}
289
+ />
258
290
  </Show>
259
291
  </nav>
260
292
 
@@ -302,6 +334,12 @@ export const HookDetails: Component = () => {
302
334
  <Show when={activeTab() === 'memory'}>
303
335
  <MemoryPanel />
304
336
  </Show>
337
+ <Show when={activeTab() === 'compaction'}>
338
+ <CompactionPanel hook={activeHook()} />
339
+ </Show>
340
+ <Show when={activeTab() === 'skills'}>
341
+ <SkillsPanel />
342
+ </Show>
305
343
  </main>
306
344
 
307
345
  <Show when={showSecondaryPane()}>
@@ -0,0 +1,125 @@
1
+ import { For, Show, createMemo } from 'solid-js'
2
+ import { useAIStore } from '../../store/ai-context'
3
+ import { useStyles } from '../../styles/use-styles'
4
+ import type { SkillsSnapshot } from '../../store/skills-registry'
5
+ import type { Message } from '../../store/ai-store'
6
+ import type { Component } from 'solid-js'
7
+
8
+ function parseSkillName(argumentsJson: string | undefined): string | undefined {
9
+ if (!argumentsJson) return undefined
10
+ try {
11
+ const parsed: unknown = JSON.parse(argumentsJson)
12
+ if (
13
+ parsed &&
14
+ typeof parsed === 'object' &&
15
+ 'name' in parsed &&
16
+ typeof parsed.name === 'string'
17
+ ) {
18
+ return parsed.name
19
+ }
20
+ } catch {
21
+ return undefined
22
+ }
23
+ return undefined
24
+ }
25
+
26
+ function loadedFromMessages(messages: Array<Message>): Array<string> {
27
+ const names = new Set<string>()
28
+ for (const message of messages) {
29
+ for (const toolCall of message.toolCalls ?? []) {
30
+ if (toolCall.name === 'load_skill') {
31
+ const name = parseSkillName(toolCall.arguments)
32
+ if (name) names.add(name)
33
+ }
34
+ }
35
+ for (const part of message.parts ?? []) {
36
+ if (part.type === 'tool-call' && part.toolName === 'load_skill') {
37
+ const name = parseSkillName(part.arguments)
38
+ if (name) names.add(name)
39
+ }
40
+ }
41
+ }
42
+ return [...names]
43
+ }
44
+
45
+ export const SkillsPanel: Component = () => {
46
+ const { state } = useAIStore()
47
+ const styles = useStyles()
48
+
49
+ const snapshot = createMemo((): SkillsSnapshot | undefined => {
50
+ const hookId = state.hooks.activeHookId
51
+ if (hookId && state.skills.snapshots[hookId]) {
52
+ return state.skills.snapshots[hookId]
53
+ }
54
+ const first = Object.values(state.skills.snapshots)[0]
55
+ return first
56
+ })
57
+
58
+ const conversation = createMemo(() => {
59
+ const hookId = state.hooks.activeHookId
60
+ if (!hookId) return undefined
61
+ return state.conversations[hookId]
62
+ })
63
+
64
+ const loaded = createMemo(() => {
65
+ const fromSnap = new Set(snapshot()?.activated ?? [])
66
+ for (const name of loadedFromMessages(conversation()?.messages ?? [])) {
67
+ fromSnap.add(name)
68
+ }
69
+ return fromSnap
70
+ })
71
+
72
+ const catalog = createMemo(() => snapshot()?.catalog ?? [])
73
+
74
+ return (
75
+ <div
76
+ class={styles().memoryPanel.container}
77
+ data-testid="ai-devtools-skills-panel"
78
+ >
79
+ <Show
80
+ when={catalog().length > 0 || loaded().size > 0}
81
+ fallback={
82
+ <div class={styles().memoryPanel.empty}>
83
+ No skills on this run. Add `withSkills(...)` to `chat()` and the
84
+ catalog will appear here. A skill lights up when the model calls
85
+ `load_skill`.
86
+ </div>
87
+ }
88
+ >
89
+ <div class={styles().memoryPanel.section}>
90
+ <div class={styles().memoryPanel.sectionTitle}>Catalog</div>
91
+ <Show
92
+ when={catalog().length > 0}
93
+ fallback={
94
+ <div class={styles().memoryPanel.sectionEmpty}>
95
+ Loaded via `load_skill`, but the catalog snapshot has not
96
+ arrived yet.
97
+ </div>
98
+ }
99
+ >
100
+ <ul class={styles().memoryPanel.list}>
101
+ <For each={catalog()}>
102
+ {(skill) => {
103
+ const isLoaded = () => loaded().has(skill.name)
104
+ return (
105
+ <li class={styles().memoryPanel.row}>
106
+ <span class={styles().memoryPanel.badge}>
107
+ {skill.name}
108
+ </span>
109
+ <Show when={isLoaded()}>
110
+ <span class={styles().memoryPanel.badge}>loaded</span>
111
+ </Show>
112
+ <span class={styles().memoryPanel.sectionEmpty}>
113
+ {skill.description}
114
+ </span>
115
+ </li>
116
+ )
117
+ }}
118
+ </For>
119
+ </ul>
120
+ </Show>
121
+ </div>
122
+ </Show>
123
+ </div>
124
+ )
125
+ }
@@ -2,4 +2,6 @@ export { HookDashboard } from './HookDashboard'
2
2
  export { HookDetails } from './HookDetails'
3
3
  export { GenerationPanel, GenerationPreview } from './GenerationPanel'
4
4
  export { MemoryPanel } from './MemoryPanel'
5
+ export { CompactionPanel } from './CompactionPanel'
6
+ export { SkillsPanel } from './SkillsPanel'
5
7
  export { ToolFixtureForm } from './ToolFixtureForm'