@tanstack/ai-devtools-core 0.5.10 → 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/BJP2PZMD.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/YJTEAPIF.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/2X67PB7B.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/2X67PB7B.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.10",
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.51.0",
88
- "@tanstack/ai-event-client": "^0.11.1"
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,7 @@ import {
39
39
  } from './preview-messages'
40
40
  import { GenerationPanel, GenerationPreview } from './GenerationPanel'
41
41
  import { MemoryPanel } from './MemoryPanel'
42
+ import { CompactionPanel } from './CompactionPanel'
42
43
  import { SkillsPanel } from './SkillsPanel'
43
44
  import type { HoverOrigin, HoverTarget, PreviewJsonItem } from './preview-model'
44
45
  import type {
@@ -50,7 +51,13 @@ import type {
50
51
  import type { Conversation, Message, ToolCall } from '../../store/ai-store'
51
52
  import type { Component, Setter } from 'solid-js'
52
53
 
53
- type DetailTab = 'conversation' | 'tools' | 'state' | 'memory' | 'skills'
54
+ type DetailTab =
55
+ | 'conversation'
56
+ | 'tools'
57
+ | 'state'
58
+ | 'memory'
59
+ | 'compaction'
60
+ | 'skills'
54
61
  type MessagePart = NonNullable<Message['parts']>[number]
55
62
  const scrollAnimations = new WeakMap<HTMLElement, number>()
56
63
 
@@ -157,6 +164,7 @@ export const HookDetails: Component = () => {
157
164
  isGenerationHook() &&
158
165
  (activeTab() === 'tools' ||
159
166
  activeTab() === 'memory' ||
167
+ activeTab() === 'compaction' ||
160
168
  activeTab() === 'skills')
161
169
  ) {
162
170
  setActiveTab('conversation')
@@ -214,8 +222,10 @@ export const HookDetails: Component = () => {
214
222
  <HookOverview
215
223
  onSelectHook={(selectedHook) => {
216
224
  selectHook(selectedHook.id)
217
- if (state.conversations[selectedHook.id]) {
218
- selectConversation(selectedHook.id)
225
+ const conversationId =
226
+ selectedHook.id || selectedHook.clientId || selectedHook.threadId
227
+ if (conversationId && state.conversations[conversationId]) {
228
+ selectConversation(conversationId)
219
229
  }
220
230
  }}
221
231
  />
@@ -265,6 +275,12 @@ export const HookDetails: Component = () => {
265
275
  activeTab={activeTab()}
266
276
  onSelect={setActiveTab}
267
277
  />
278
+ <TabButton
279
+ label="Compaction"
280
+ tab="compaction"
281
+ activeTab={activeTab()}
282
+ onSelect={setActiveTab}
283
+ />
268
284
  <TabButton
269
285
  label="Skills"
270
286
  tab="skills"
@@ -318,6 +334,9 @@ export const HookDetails: Component = () => {
318
334
  <Show when={activeTab() === 'memory'}>
319
335
  <MemoryPanel />
320
336
  </Show>
337
+ <Show when={activeTab() === 'compaction'}>
338
+ <CompactionPanel hook={activeHook()} />
339
+ </Show>
321
340
  <Show when={activeTab() === 'skills'}>
322
341
  <SkillsPanel />
323
342
  </Show>
@@ -2,5 +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'
5
6
  export { SkillsPanel } from './SkillsPanel'
6
7
  export { ToolFixtureForm } from './ToolFixtureForm'
@@ -6,9 +6,9 @@ import {
6
6
  applyHookEvent,
7
7
  clearHookRegistry,
8
8
  createHookRegistryState,
9
+ markHookViewed,
9
10
  removeSavedFixture,
10
11
  replaceSavedFixtures,
11
- setActiveHook,
12
12
  } from './hook-registry'
13
13
  import {
14
14
  createClientToolCallMessage,
@@ -20,6 +20,11 @@ import {
20
20
  clearMemoryRegistry,
21
21
  createMemoryRegistryState,
22
22
  } from './memory-registry'
23
+ import {
24
+ applyCompactionEvent,
25
+ clearCompactionRegistry,
26
+ createCompactionRegistryState,
27
+ } from './compaction-registry'
23
28
  import {
24
29
  applySkillsSnapshot,
25
30
  clearSkillsRegistry,
@@ -33,6 +38,7 @@ import type {
33
38
  } from '@tanstack/ai-event-client'
34
39
  import type { HookRegistryState, ToolFixtureRecord } from './hook-registry'
35
40
  import type { MemoryRegistryState } from './memory-registry'
41
+ import type { CompactionRegistryState } from './compaction-registry'
36
42
  import type { SkillsRegistryState } from './skills-registry'
37
43
  import type { ParentComponent } from 'solid-js'
38
44
 
@@ -242,6 +248,7 @@ interface AIStoreState {
242
248
  activeConversationId: string | null
243
249
  hooks: HookRegistryState
244
250
  memory: MemoryRegistryState
251
+ compaction: CompactionRegistryState
245
252
  skills: SkillsRegistryState
246
253
  }
247
254
 
@@ -251,6 +258,7 @@ interface AIContextValue {
251
258
  selectConversation: (id: string) => void
252
259
  clearHooks: () => void
253
260
  clearMemory: () => void
261
+ clearCompaction: () => void
254
262
  clearSkills: () => void
255
263
  selectHook: (id: string | null) => void
256
264
  saveToolFixture: (fixture: ToolFixtureRecord) => void
@@ -274,6 +282,7 @@ export const AIProvider: ParentComponent = (props) => {
274
282
  activeConversationId: null,
275
283
  hooks: createHookRegistryState(),
276
284
  memory: createMemoryRegistryState(),
285
+ compaction: createCompactionRegistryState(),
277
286
  skills: createSkillsRegistryState(),
278
287
  })
279
288
 
@@ -670,6 +679,15 @@ export const AIProvider: ParentComponent = (props) => {
670
679
  )
671
680
  }
672
681
 
682
+ function clearCompaction() {
683
+ setState(
684
+ 'compaction',
685
+ produce((compaction: CompactionRegistryState) => {
686
+ clearCompactionRegistry(compaction)
687
+ }),
688
+ )
689
+ }
690
+
673
691
  function clearSkills() {
674
692
  setState(
675
693
  'skills',
@@ -680,12 +698,15 @@ export const AIProvider: ParentComponent = (props) => {
680
698
  }
681
699
 
682
700
  function selectHook(id: string | null) {
683
- setState(
684
- 'hooks',
685
- produce((hooks: HookRegistryState) => {
686
- setActiveHook(hooks, id)
687
- }),
688
- )
701
+ setState('hooks', 'activeHookId', id)
702
+ if (id) {
703
+ setState(
704
+ 'hooks',
705
+ produce((hooks: HookRegistryState) => {
706
+ markHookViewed(hooks, id)
707
+ }),
708
+ )
709
+ }
689
710
  }
690
711
 
691
712
  function saveToolFixture(fixture: ToolFixtureRecord) {
@@ -2984,6 +3005,90 @@ export const AIProvider: ParentComponent = (props) => {
2984
3005
  }),
2985
3006
  )
2986
3007
 
3008
+ const recordCompactionLifecycle = (
3009
+ kind: 'started' | 'state' | 'ended',
3010
+ hookName: 'onCompactStart' | 'onCompact' | 'onCompactEnd',
3011
+ payload: {
3012
+ timestamp: number
3013
+ requestId?: string
3014
+ streamId?: string
3015
+ clientId?: string
3016
+ hookId?: string
3017
+ threadId?: string
3018
+ runId?: string
3019
+ eventId?: string
3020
+ before?: number
3021
+ after?: number
3022
+ messagesBefore?: number
3023
+ messagesAfter?: number
3024
+ reusedCheckpoint?: boolean
3025
+ maxTokens?: number
3026
+ strategyKey?: string
3027
+ durationMs?: number
3028
+ dropped?: CompactionRegistryState['events'][number]['dropped']
3029
+ result?: CompactionRegistryState['events'][number]['result']
3030
+ },
3031
+ ) => {
3032
+ setState(
3033
+ 'compaction',
3034
+ produce((compaction: CompactionRegistryState) => {
3035
+ applyCompactionEvent(compaction, kind, payload)
3036
+ }),
3037
+ )
3038
+
3039
+ const { requestId, streamId, clientId } = payload
3040
+ const conversationId =
3041
+ (clientId && state.conversations[clientId] ? clientId : undefined) ||
3042
+ (streamId ? streamToConversation.get(streamId) : undefined) ||
3043
+ (requestId ? requestToConversation.get(requestId) : undefined)
3044
+ if (!conversationId || !state.conversations[conversationId]) return
3045
+
3046
+ const conv = state.conversations[conversationId]
3047
+ const iterIndex = findLatestIterationIndex(conv, requestId)
3048
+ if (iterIndex < 0) return
3049
+
3050
+ const mwEvent: MiddlewareEvent = {
3051
+ id: `mw-cmp-${kind}-${Date.now()}-${Math.random()}`,
3052
+ middlewareName: 'compaction',
3053
+ hookName,
3054
+ timestamp: payload.timestamp,
3055
+ hasTransform: kind === 'state',
3056
+ configChanges: {
3057
+ before: payload.before,
3058
+ after: payload.after,
3059
+ messagesBefore: payload.messagesBefore,
3060
+ messagesAfter: payload.messagesAfter,
3061
+ reusedCheckpoint: payload.reusedCheckpoint,
3062
+ maxTokens: payload.maxTokens,
3063
+ strategyKey: payload.strategyKey,
3064
+ durationMs: payload.durationMs,
3065
+ },
3066
+ }
3067
+
3068
+ setState(
3069
+ 'conversations',
3070
+ conversationId,
3071
+ 'iterations',
3072
+ iterIndex,
3073
+ 'middlewareEvents',
3074
+ produce((arr: Array<MiddlewareEvent>) => {
3075
+ arr.push(mwEvent)
3076
+ }),
3077
+ )
3078
+ }
3079
+
3080
+ cleanupFns.push(
3081
+ aiEventClient.on('compaction:started', (e) => {
3082
+ recordCompactionLifecycle('started', 'onCompactStart', e.payload)
3083
+ }),
3084
+ aiEventClient.on('compaction:state', (e) => {
3085
+ recordCompactionLifecycle('state', 'onCompact', e.payload)
3086
+ }),
3087
+ aiEventClient.on('compaction:ended', (e) => {
3088
+ recordCompactionLifecycle('ended', 'onCompactEnd', e.payload)
3089
+ }),
3090
+ )
3091
+
2987
3092
  cleanupFns.push(
2988
3093
  aiEventClient.on('summarize:request:started', (e) => {
2989
3094
  const { requestId, model, inputLength, timestamp, clientId } = e.payload
@@ -3491,6 +3596,7 @@ export const AIProvider: ParentComponent = (props) => {
3491
3596
  selectConversation,
3492
3597
  clearHooks,
3493
3598
  clearMemory,
3599
+ clearCompaction,
3494
3600
  clearSkills,
3495
3601
  selectHook,
3496
3602
  saveToolFixture,