@tanstack/ai-devtools-core 0.3.37 → 0.4.0
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/DKCFIT6H.js +12142 -0
- package/dist/Shell/SLOX2TPU.js +12119 -0
- package/dist/Shell/Z6HLJOVZ.js +10170 -0
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/DKCFIT6H.js +12142 -0
- package/dist/production/Shell/SLOX2TPU.js +12119 -0
- package/dist/production/Shell/Z6HLJOVZ.js +10170 -0
- 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 +4 -4
- package/src/components/Shell.tsx +74 -30
- package/src/components/conversation/IterationCard.tsx +261 -12
- package/src/components/conversation/IterationTimeline.tsx +66 -16
- package/src/components/conversation/index.ts +0 -9
- package/src/components/hooks/FixtureNamePopover.tsx +69 -0
- package/src/components/hooks/GenerationPanel.tsx +847 -0
- package/src/components/hooks/HookDashboard.tsx +292 -0
- package/src/components/hooks/HookDetails.tsx +1757 -0
- package/src/components/hooks/ToolFixtureForm.tsx +347 -0
- package/src/components/hooks/hook-dashboard-model.ts +134 -0
- package/src/components/hooks/index.ts +4 -0
- package/src/components/hooks/preview-messages.ts +117 -0
- package/src/components/hooks/preview-model.ts +195 -0
- package/src/components/utils/format.ts +0 -58
- package/src/store/ai-context.tsx +1165 -159
- package/src/store/ai-store.ts +0 -1
- package/src/store/hook-registry.ts +815 -0
- package/src/store/message-event-utils.ts +60 -0
- package/src/styles/use-styles.ts +1558 -99
- package/dist/Shell/MRWZEHAJ.js +0 -6225
- package/dist/Shell/RRRNZJ5E.js +0 -7560
- package/dist/production/Shell/MRWZEHAJ.js +0 -6225
- package/dist/production/Shell/RRRNZJ5E.js +0 -7560
- package/src/components/ConversationDetails.tsx +0 -216
- package/src/components/ConversationsList.tsx +0 -21
- package/src/components/conversation/ActivityEventsTab.tsx +0 -57
- package/src/components/conversation/ChunkBadges.tsx +0 -51
- package/src/components/conversation/ChunkItem.tsx +0 -273
- package/src/components/conversation/ChunksCollapsible.tsx +0 -56
- package/src/components/conversation/ChunksTab.tsx +0 -87
- package/src/components/conversation/ConversationHeader.tsx +0 -106
- package/src/components/conversation/ConversationTabs.tsx +0 -205
- package/src/components/conversation/MessageCard.tsx +0 -174
- package/src/components/conversation/MessageGroup.tsx +0 -101
- package/src/components/conversation/MessagesTab.tsx +0 -30
- package/src/components/conversation/SummariesTab.tsx +0 -91
- package/src/components/conversation/ToolCallDisplay.tsx +0 -99
- package/src/components/list/ConversationRow.tsx +0 -85
- package/src/components/list/index.ts +0 -1
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AIDevtoolsEventEnvelope,
|
|
3
|
+
AIDevtoolsEventSource,
|
|
4
|
+
AIDevtoolsEventVisibility,
|
|
5
|
+
DevtoolsToolFixtureApplyEvent,
|
|
6
|
+
HookRegisteredEvent,
|
|
7
|
+
HookStateSnapshotEvent,
|
|
8
|
+
HookUnregisteredEvent,
|
|
9
|
+
HookUpdatedEvent,
|
|
10
|
+
RunLifecycleEvent,
|
|
11
|
+
ToolsRegisteredEvent,
|
|
12
|
+
} from '@tanstack/ai-event-client'
|
|
13
|
+
|
|
14
|
+
export type HookOutputKind =
|
|
15
|
+
| 'chat'
|
|
16
|
+
| 'text'
|
|
17
|
+
| 'structured'
|
|
18
|
+
| 'image'
|
|
19
|
+
| 'video'
|
|
20
|
+
| 'audio'
|
|
21
|
+
|
|
22
|
+
export type HookLifecycle =
|
|
23
|
+
| 'mounted'
|
|
24
|
+
| 'active'
|
|
25
|
+
| 'streaming'
|
|
26
|
+
| 'errored'
|
|
27
|
+
| 'stale'
|
|
28
|
+
|
|
29
|
+
export interface RegisteredTool {
|
|
30
|
+
name: string
|
|
31
|
+
description?: string
|
|
32
|
+
inputSchema?: unknown
|
|
33
|
+
outputSchema?: unknown
|
|
34
|
+
needsApproval?: boolean
|
|
35
|
+
metadata?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ToolFixtureRecord {
|
|
39
|
+
id: string
|
|
40
|
+
createdAt: number
|
|
41
|
+
name?: string
|
|
42
|
+
hookId?: string
|
|
43
|
+
threadId?: string
|
|
44
|
+
runId?: string
|
|
45
|
+
toolName: string
|
|
46
|
+
input: unknown
|
|
47
|
+
output: unknown
|
|
48
|
+
execute?: boolean
|
|
49
|
+
message?: ToolFixtureMessage
|
|
50
|
+
toolCallId?: string
|
|
51
|
+
messageId?: string
|
|
52
|
+
errorText?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ToolFixtureMessage {
|
|
56
|
+
id: string
|
|
57
|
+
role: 'system' | 'user' | 'assistant'
|
|
58
|
+
parts: Array<unknown>
|
|
59
|
+
createdAt?: number | string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ToolFixtureRecordDraft {
|
|
63
|
+
id?: string
|
|
64
|
+
createdAt?: number
|
|
65
|
+
name?: string
|
|
66
|
+
hookId?: string
|
|
67
|
+
threadId?: string
|
|
68
|
+
runId?: string
|
|
69
|
+
toolName: string
|
|
70
|
+
input: unknown
|
|
71
|
+
output?: unknown
|
|
72
|
+
execute?: boolean
|
|
73
|
+
message?: ToolFixtureMessage
|
|
74
|
+
toolCallId?: string
|
|
75
|
+
messageId?: string
|
|
76
|
+
errorText?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TimelineEvent {
|
|
80
|
+
id: string
|
|
81
|
+
eventType: string
|
|
82
|
+
timestamp: number
|
|
83
|
+
source?: AIDevtoolsEventSource
|
|
84
|
+
visibility?: AIDevtoolsEventVisibility
|
|
85
|
+
runtimeId?: string
|
|
86
|
+
hookId?: string
|
|
87
|
+
threadId?: string
|
|
88
|
+
runId?: string
|
|
89
|
+
messageId?: string
|
|
90
|
+
toolCallId?: string
|
|
91
|
+
payload: unknown
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface RunRecord {
|
|
95
|
+
id: string
|
|
96
|
+
hookId?: string
|
|
97
|
+
threadId?: string
|
|
98
|
+
status: RunLifecycleEvent['status']
|
|
99
|
+
source?: AIDevtoolsEventSource
|
|
100
|
+
visibility?: AIDevtoolsEventVisibility
|
|
101
|
+
startedAt: number
|
|
102
|
+
updatedAt: number
|
|
103
|
+
completedAt?: number
|
|
104
|
+
error?: string
|
|
105
|
+
eventIds: Array<string>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface HookRecord {
|
|
109
|
+
id: string
|
|
110
|
+
hookName: string
|
|
111
|
+
displayName?: string
|
|
112
|
+
framework?: string
|
|
113
|
+
outputKind?: HookOutputKind
|
|
114
|
+
lifecycle: HookLifecycle
|
|
115
|
+
clientId?: string
|
|
116
|
+
threadId?: string
|
|
117
|
+
correlationId?: string
|
|
118
|
+
registeredAt: number
|
|
119
|
+
updatedAt: number
|
|
120
|
+
unregisteredAt?: number
|
|
121
|
+
state: Record<string, unknown>
|
|
122
|
+
tools: Array<RegisteredTool>
|
|
123
|
+
runIds: Array<string>
|
|
124
|
+
eventIds: Array<string>
|
|
125
|
+
activityRunIds: Array<string>
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cap on the number of recently-seen event IDs we retain for dedupe. Once
|
|
130
|
+
* exceeded, the oldest entries are evicted FIFO so the structure stays
|
|
131
|
+
* bounded for the life of the session. Events older than this window may
|
|
132
|
+
* dedupe-miss; consumers should not rely on perfect global dedupe.
|
|
133
|
+
*/
|
|
134
|
+
export const SEEN_EVENT_IDS_CAP = 10_000
|
|
135
|
+
|
|
136
|
+
export interface HookRegistryState {
|
|
137
|
+
hooks: Record<string, HookRecord>
|
|
138
|
+
runs: Record<string, RunRecord>
|
|
139
|
+
events: Record<string, TimelineEvent>
|
|
140
|
+
fixtures: Array<ToolFixtureRecord>
|
|
141
|
+
activeHookId: string | null
|
|
142
|
+
seenEventIds: Record<string, true>
|
|
143
|
+
seenEventIdOrder: Array<string>
|
|
144
|
+
seenHookActivityCounts: Record<string, number>
|
|
145
|
+
unregisteredHookIds: Record<string, true>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createHookRegistryState(): HookRegistryState {
|
|
149
|
+
return {
|
|
150
|
+
hooks: {},
|
|
151
|
+
runs: {},
|
|
152
|
+
events: {},
|
|
153
|
+
fixtures: [],
|
|
154
|
+
activeHookId: null,
|
|
155
|
+
seenEventIds: {},
|
|
156
|
+
seenEventIdOrder: [],
|
|
157
|
+
seenHookActivityCounts: {},
|
|
158
|
+
unregisteredHookIds: {},
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type KnownHookEvent =
|
|
163
|
+
| HookRegisteredEvent
|
|
164
|
+
| HookUpdatedEvent
|
|
165
|
+
| HookUnregisteredEvent
|
|
166
|
+
| HookStateSnapshotEvent
|
|
167
|
+
| RunLifecycleEvent
|
|
168
|
+
| ToolsRegisteredEvent
|
|
169
|
+
| DevtoolsToolFixtureApplyEvent
|
|
170
|
+
|
|
171
|
+
type RuntimeScopedHookEvent = KnownHookEvent & { runtimeId?: string }
|
|
172
|
+
|
|
173
|
+
interface HookUpsertEvent extends Partial<
|
|
174
|
+
Pick<
|
|
175
|
+
HookRegisteredEvent,
|
|
176
|
+
| 'clientId'
|
|
177
|
+
| 'displayName'
|
|
178
|
+
| 'framework'
|
|
179
|
+
| 'outputKind'
|
|
180
|
+
| 'source'
|
|
181
|
+
| 'threadId'
|
|
182
|
+
| 'visibility'
|
|
183
|
+
| 'correlationId'
|
|
184
|
+
>
|
|
185
|
+
> {
|
|
186
|
+
hookId: string
|
|
187
|
+
hookName: string
|
|
188
|
+
lifecycle: HookLifecycle
|
|
189
|
+
timestamp: number
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function markEventSeen(
|
|
193
|
+
state: HookRegistryState,
|
|
194
|
+
eventName: string,
|
|
195
|
+
event: Partial<AIDevtoolsEventEnvelope> & {
|
|
196
|
+
runtimeId?: string
|
|
197
|
+
timestamp?: number
|
|
198
|
+
},
|
|
199
|
+
): boolean {
|
|
200
|
+
let key: string
|
|
201
|
+
if (event.eventId) {
|
|
202
|
+
key = event.eventId
|
|
203
|
+
} else {
|
|
204
|
+
key = [
|
|
205
|
+
eventName,
|
|
206
|
+
event.source ?? 'unknown',
|
|
207
|
+
event.visibility ?? 'unknown',
|
|
208
|
+
event.runtimeId ?? 'no-runtime',
|
|
209
|
+
event.hookId ?? event.clientId ?? 'no-hook',
|
|
210
|
+
event.threadId ?? 'no-thread',
|
|
211
|
+
event.runId ?? 'no-run',
|
|
212
|
+
event.messageId ?? 'no-message',
|
|
213
|
+
event.toolCallId ?? 'no-tool-call',
|
|
214
|
+
event.timestamp ?? 'no-time',
|
|
215
|
+
].join(':')
|
|
216
|
+
// If every identifying field fell back to its literal sentinel the synthesised
|
|
217
|
+
// key is just `eventName:unknown:unknown:no-runtime:...` — useful for very
|
|
218
|
+
// little. Warn so it's obvious in the console why deduplication may be
|
|
219
|
+
// collapsing distinct events together.
|
|
220
|
+
if (
|
|
221
|
+
!event.source &&
|
|
222
|
+
!event.visibility &&
|
|
223
|
+
!event.runtimeId &&
|
|
224
|
+
!event.hookId &&
|
|
225
|
+
!event.clientId &&
|
|
226
|
+
!event.threadId &&
|
|
227
|
+
!event.runId &&
|
|
228
|
+
!event.messageId &&
|
|
229
|
+
!event.toolCallId &&
|
|
230
|
+
!event.timestamp
|
|
231
|
+
) {
|
|
232
|
+
console.warn(
|
|
233
|
+
`[ai-devtools] dedupe key for "${eventName}" has no identifying fields; events may collide.`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (state.seenEventIds[key]) {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
state.seenEventIds[key] = true
|
|
242
|
+
state.seenEventIdOrder.push(key)
|
|
243
|
+
if (state.seenEventIdOrder.length > SEEN_EVENT_IDS_CAP) {
|
|
244
|
+
const evicted = state.seenEventIdOrder.shift()
|
|
245
|
+
if (evicted !== undefined) {
|
|
246
|
+
delete state.seenEventIds[evicted]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return true
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function applyHookEvent(
|
|
253
|
+
state: HookRegistryState,
|
|
254
|
+
eventName: string,
|
|
255
|
+
event: RuntimeScopedHookEvent,
|
|
256
|
+
): void {
|
|
257
|
+
if (isForeignClientRuntimeEvent(event)) {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!markEventSeen(state, eventName, event)) {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const timelineEvent = createTimelineEvent(eventName, event)
|
|
266
|
+
state.events[timelineEvent.id] = timelineEvent
|
|
267
|
+
|
|
268
|
+
switch (eventName) {
|
|
269
|
+
case 'hook:registered': {
|
|
270
|
+
const registered = event as HookRegisteredEvent
|
|
271
|
+
delete state.unregisteredHookIds[registered.hookId]
|
|
272
|
+
upsertHook(state, registered)
|
|
273
|
+
attachEventToHook(state, registered.hookId, timelineEvent.id)
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
case 'hook:updated': {
|
|
277
|
+
const updated = event as HookUpdatedEvent
|
|
278
|
+
if (state.unregisteredHookIds[updated.hookId]) {
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
if (isStaleHookInstanceEvent(state, updated.hookId, updated)) {
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
upsertHook(state, updated)
|
|
285
|
+
attachEventToHook(state, updated.hookId, timelineEvent.id)
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
case 'hook:unregistered': {
|
|
289
|
+
const unregistered = event as HookUnregisteredEvent
|
|
290
|
+
const existing = state.hooks[unregistered.hookId]
|
|
291
|
+
if (
|
|
292
|
+
existing?.clientId &&
|
|
293
|
+
unregistered.clientId &&
|
|
294
|
+
existing.clientId !== unregistered.clientId
|
|
295
|
+
) {
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
if (
|
|
299
|
+
existing?.correlationId &&
|
|
300
|
+
unregistered.correlationId &&
|
|
301
|
+
existing.correlationId !== unregistered.correlationId
|
|
302
|
+
) {
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
if (existing && existing.registeredAt > unregistered.timestamp) {
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
state.unregisteredHookIds[unregistered.hookId] = true
|
|
309
|
+
removeHookRecord(state, unregistered.hookId)
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
case 'hook:state-snapshot': {
|
|
313
|
+
const snapshot = event as HookStateSnapshotEvent
|
|
314
|
+
if (state.unregisteredHookIds[snapshot.hookId]) {
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
if (isStaleHookInstanceEvent(state, snapshot.hookId, snapshot)) {
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
upsertHook(state, {
|
|
321
|
+
...snapshot,
|
|
322
|
+
lifecycle: inferLifecycleFromSnapshot(snapshot.state),
|
|
323
|
+
})
|
|
324
|
+
const hook = state.hooks[snapshot.hookId]
|
|
325
|
+
if (hook) {
|
|
326
|
+
hook.state = snapshot.state
|
|
327
|
+
hook.updatedAt = snapshot.timestamp
|
|
328
|
+
}
|
|
329
|
+
attachEventToHook(state, snapshot.hookId, timelineEvent.id)
|
|
330
|
+
// Generation hooks ship their run history inside the snapshot. When
|
|
331
|
+
// devtools is opened after a run has already completed, the run
|
|
332
|
+
// lifecycle events fired before mount are lost, so backfill the
|
|
333
|
+
// global runs map and the hook's runIds from the snapshot itself.
|
|
334
|
+
syncRunsFromSnapshot(state, snapshot, timelineEvent.id)
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
case 'tools:registered': {
|
|
338
|
+
const toolsEvent = event as ToolsRegisteredEvent
|
|
339
|
+
if (state.unregisteredHookIds[toolsEvent.hookId]) {
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
if (isStaleHookInstanceEvent(state, toolsEvent.hookId, toolsEvent)) {
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
if (!toolsEvent.hookName) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`[ai-devtools] tools:registered event for hook "${toolsEvent.hookId}" had no hookName; displaying raw hookId in the UI.`,
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
upsertHook(state, {
|
|
351
|
+
...toolsEvent,
|
|
352
|
+
hookName: toolsEvent.hookName ?? toolsEvent.hookId,
|
|
353
|
+
lifecycle: 'active',
|
|
354
|
+
})
|
|
355
|
+
const hook = state.hooks[toolsEvent.hookId]
|
|
356
|
+
if (hook) {
|
|
357
|
+
hook.tools = toolsEvent.tools
|
|
358
|
+
hook.updatedAt = toolsEvent.timestamp
|
|
359
|
+
}
|
|
360
|
+
attachEventToHook(state, toolsEvent.hookId, timelineEvent.id)
|
|
361
|
+
break
|
|
362
|
+
}
|
|
363
|
+
case 'run:created':
|
|
364
|
+
case 'run:started':
|
|
365
|
+
case 'run:updated':
|
|
366
|
+
case 'run:completed':
|
|
367
|
+
case 'run:errored':
|
|
368
|
+
case 'run:cancelled': {
|
|
369
|
+
const runEvent = event as RunLifecycleEvent
|
|
370
|
+
if (runEvent.hookId && state.unregisteredHookIds[runEvent.hookId]) {
|
|
371
|
+
break
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
runEvent.hookId &&
|
|
375
|
+
isStaleHookInstanceEvent(state, runEvent.hookId, runEvent)
|
|
376
|
+
) {
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
upsertRun(state, runEvent, timelineEvent.id)
|
|
380
|
+
if (runEvent.hookId) {
|
|
381
|
+
upsertUnknownHook(state, runEvent.hookId, runEvent)
|
|
382
|
+
attachRunToHook(state, runEvent.hookId, runEvent.runId)
|
|
383
|
+
attachActivityRunToHook(state, runEvent.hookId, runEvent.runId)
|
|
384
|
+
attachEventToHook(state, runEvent.hookId, timelineEvent.id)
|
|
385
|
+
}
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
case 'devtools:tool-fixture:apply': {
|
|
389
|
+
const fixtureEvent = event as DevtoolsToolFixtureApplyEvent
|
|
390
|
+
if (fixtureEvent.hookId) {
|
|
391
|
+
attachEventToHook(state, fixtureEvent.hookId, timelineEvent.id)
|
|
392
|
+
}
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (state.activeHookId && !state.hooks[state.activeHookId]) {
|
|
398
|
+
state.activeHookId = null
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (state.activeHookId) {
|
|
402
|
+
markHookViewed(state, state.activeHookId)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function setActiveHook(
|
|
407
|
+
state: HookRegistryState,
|
|
408
|
+
hookId: string | null,
|
|
409
|
+
): void {
|
|
410
|
+
state.activeHookId = hookId
|
|
411
|
+
if (hookId) {
|
|
412
|
+
markHookViewed(state, hookId)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function clearHookRegistry(state: HookRegistryState): void {
|
|
417
|
+
state.hooks = {}
|
|
418
|
+
state.runs = {}
|
|
419
|
+
state.events = {}
|
|
420
|
+
state.activeHookId = null
|
|
421
|
+
state.seenEventIds = {}
|
|
422
|
+
state.seenEventIdOrder = []
|
|
423
|
+
state.seenHookActivityCounts = {}
|
|
424
|
+
state.unregisteredHookIds = {}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function markHookViewed(state: HookRegistryState, hookId: string): void {
|
|
428
|
+
const hook = state.hooks[hookId]
|
|
429
|
+
if (!hook) return
|
|
430
|
+
state.seenHookActivityCounts[hookId] = hook.activityRunIds.length
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function getHookUnseenEventCount(
|
|
434
|
+
state: HookRegistryState,
|
|
435
|
+
hookId: string,
|
|
436
|
+
): number {
|
|
437
|
+
const hook = state.hooks[hookId]
|
|
438
|
+
if (!hook) return 0
|
|
439
|
+
const seenCount = state.seenHookActivityCounts[hookId] ?? 0
|
|
440
|
+
return Math.max(0, hook.activityRunIds.length - seenCount)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function addSavedFixture(
|
|
444
|
+
state: HookRegistryState,
|
|
445
|
+
fixture: ToolFixtureRecord,
|
|
446
|
+
): void {
|
|
447
|
+
state.fixtures = [
|
|
448
|
+
fixture,
|
|
449
|
+
...state.fixtures.filter((item) => item.id !== fixture.id),
|
|
450
|
+
].slice(0, 50)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function removeSavedFixture(
|
|
454
|
+
state: HookRegistryState,
|
|
455
|
+
fixtureId: string,
|
|
456
|
+
): void {
|
|
457
|
+
state.fixtures = state.fixtures.filter((fixture) => fixture.id !== fixtureId)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function replaceSavedFixtures(
|
|
461
|
+
state: HookRegistryState,
|
|
462
|
+
fixtures: Array<ToolFixtureRecord>,
|
|
463
|
+
): void {
|
|
464
|
+
state.fixtures = fixtures.slice(0, 50)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function createToolFixtureRecord(
|
|
468
|
+
draft: ToolFixtureRecordDraft,
|
|
469
|
+
): ToolFixtureRecord {
|
|
470
|
+
const createdAt = draft.createdAt ?? Date.now()
|
|
471
|
+
const fixtureScope = draft.hookId ?? draft.threadId ?? 'global'
|
|
472
|
+
const fixtureKey = draft.toolCallId ?? createdAt
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
id: draft.id ?? `fixture:${fixtureScope}:${draft.toolName}:${fixtureKey}`,
|
|
476
|
+
createdAt,
|
|
477
|
+
...(draft.name ? { name: draft.name } : {}),
|
|
478
|
+
...(draft.hookId ? { hookId: draft.hookId } : {}),
|
|
479
|
+
...(draft.threadId ? { threadId: draft.threadId } : {}),
|
|
480
|
+
...(draft.runId ? { runId: draft.runId } : {}),
|
|
481
|
+
toolName: draft.toolName,
|
|
482
|
+
input: draft.input,
|
|
483
|
+
output: draft.output ?? null,
|
|
484
|
+
...(draft.execute !== undefined ? { execute: draft.execute } : {}),
|
|
485
|
+
...(draft.message ? { message: draft.message } : {}),
|
|
486
|
+
...(draft.toolCallId ? { toolCallId: draft.toolCallId } : {}),
|
|
487
|
+
...(draft.messageId ? { messageId: draft.messageId } : {}),
|
|
488
|
+
...(draft.errorText ? { errorText: draft.errorText } : {}),
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function createTimelineEvent(
|
|
493
|
+
eventType: string,
|
|
494
|
+
event: RuntimeScopedHookEvent,
|
|
495
|
+
): TimelineEvent {
|
|
496
|
+
return {
|
|
497
|
+
id: event.eventId ?? `${eventType}:${event.timestamp}:${Math.random()}`,
|
|
498
|
+
eventType,
|
|
499
|
+
timestamp: event.timestamp,
|
|
500
|
+
...(event.source ? { source: event.source } : {}),
|
|
501
|
+
...(event.visibility ? { visibility: event.visibility } : {}),
|
|
502
|
+
...(event.runtimeId ? { runtimeId: event.runtimeId } : {}),
|
|
503
|
+
...(event.hookId ? { hookId: event.hookId } : {}),
|
|
504
|
+
...(event.threadId ? { threadId: event.threadId } : {}),
|
|
505
|
+
...(event.runId ? { runId: event.runId } : {}),
|
|
506
|
+
...(event.messageId ? { messageId: event.messageId } : {}),
|
|
507
|
+
...(event.toolCallId ? { toolCallId: event.toolCallId } : {}),
|
|
508
|
+
payload: event,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function isForeignClientRuntimeEvent(event: RuntimeScopedHookEvent): boolean {
|
|
513
|
+
return (
|
|
514
|
+
event.source === 'client' &&
|
|
515
|
+
typeof event.runtimeId === 'string' &&
|
|
516
|
+
event.runtimeId !== getLocalAIDevtoolsRuntimeId()
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
declare global {
|
|
521
|
+
var __TANSTACK_AI_DEVTOOLS_RUNTIME_ID__: string | undefined
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function getLocalAIDevtoolsRuntimeId(): string {
|
|
525
|
+
if (!globalThis.__TANSTACK_AI_DEVTOOLS_RUNTIME_ID__) {
|
|
526
|
+
globalThis.__TANSTACK_AI_DEVTOOLS_RUNTIME_ID__ = createRuntimeId()
|
|
527
|
+
}
|
|
528
|
+
return globalThis.__TANSTACK_AI_DEVTOOLS_RUNTIME_ID__
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function createRuntimeId(): string {
|
|
532
|
+
const cryptoLike = (
|
|
533
|
+
globalThis as {
|
|
534
|
+
crypto?: {
|
|
535
|
+
randomUUID?: () => string
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
).crypto
|
|
539
|
+
if (cryptoLike?.randomUUID) {
|
|
540
|
+
return cryptoLike.randomUUID()
|
|
541
|
+
}
|
|
542
|
+
return Math.random().toString(36).slice(2)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function upsertHook(state: HookRegistryState, event: HookUpsertEvent): void {
|
|
546
|
+
const existing = state.hooks[event.hookId]
|
|
547
|
+
if (!existing) {
|
|
548
|
+
state.hooks[event.hookId] = {
|
|
549
|
+
id: event.hookId,
|
|
550
|
+
hookName: event.hookName,
|
|
551
|
+
...(event.displayName ? { displayName: event.displayName } : {}),
|
|
552
|
+
...(event.framework ? { framework: event.framework } : {}),
|
|
553
|
+
...(event.outputKind ? { outputKind: event.outputKind } : {}),
|
|
554
|
+
lifecycle: event.lifecycle,
|
|
555
|
+
...(event.clientId ? { clientId: event.clientId } : {}),
|
|
556
|
+
...(event.threadId ? { threadId: event.threadId } : {}),
|
|
557
|
+
...(event.correlationId ? { correlationId: event.correlationId } : {}),
|
|
558
|
+
registeredAt: event.timestamp,
|
|
559
|
+
updatedAt: event.timestamp,
|
|
560
|
+
state: {},
|
|
561
|
+
tools: [],
|
|
562
|
+
runIds: [],
|
|
563
|
+
eventIds: [],
|
|
564
|
+
activityRunIds: [],
|
|
565
|
+
}
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
existing.hookName = event.hookName
|
|
570
|
+
if (event.displayName) existing.displayName = event.displayName
|
|
571
|
+
if (event.framework) existing.framework = event.framework
|
|
572
|
+
if (event.outputKind) existing.outputKind = event.outputKind
|
|
573
|
+
if (event.clientId) existing.clientId = event.clientId
|
|
574
|
+
if (event.threadId) existing.threadId = event.threadId
|
|
575
|
+
if (event.correlationId) existing.correlationId = event.correlationId
|
|
576
|
+
existing.lifecycle = event.lifecycle
|
|
577
|
+
existing.updatedAt = event.timestamp
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isStaleHookInstanceEvent(
|
|
581
|
+
state: HookRegistryState,
|
|
582
|
+
hookId: string,
|
|
583
|
+
event: RuntimeScopedHookEvent,
|
|
584
|
+
): boolean {
|
|
585
|
+
const existing = state.hooks[hookId]
|
|
586
|
+
return Boolean(
|
|
587
|
+
existing?.correlationId &&
|
|
588
|
+
event.correlationId &&
|
|
589
|
+
existing.correlationId !== event.correlationId,
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function upsertUnknownHook(
|
|
594
|
+
state: HookRegistryState,
|
|
595
|
+
hookId: string,
|
|
596
|
+
event: RunLifecycleEvent,
|
|
597
|
+
): void {
|
|
598
|
+
if (state.hooks[hookId]) return
|
|
599
|
+
state.hooks[hookId] = {
|
|
600
|
+
id: hookId,
|
|
601
|
+
hookName: hookId,
|
|
602
|
+
lifecycle: 'active',
|
|
603
|
+
...(event.clientId ? { clientId: event.clientId } : {}),
|
|
604
|
+
...(event.threadId ? { threadId: event.threadId } : {}),
|
|
605
|
+
registeredAt: event.timestamp,
|
|
606
|
+
updatedAt: event.timestamp,
|
|
607
|
+
state: {},
|
|
608
|
+
tools: [],
|
|
609
|
+
runIds: [],
|
|
610
|
+
eventIds: [],
|
|
611
|
+
activityRunIds: [],
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function removeHookRecord(state: HookRegistryState, hookId: string): void {
|
|
616
|
+
const hook = state.hooks[hookId]
|
|
617
|
+
const hookRunIds = new Set(hook?.runIds ?? [])
|
|
618
|
+
const hookEventIds = new Set(hook?.eventIds ?? [])
|
|
619
|
+
|
|
620
|
+
for (const [runId, run] of Object.entries(state.runs)) {
|
|
621
|
+
if (run.hookId === hookId || hookRunIds.has(runId)) {
|
|
622
|
+
delete state.runs[runId]
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Only remove events that were attached to the hook via attachEventToHook;
|
|
627
|
+
// the unregister event itself is intentionally not attached so it survives
|
|
628
|
+
// and can still be inspected on the global timeline.
|
|
629
|
+
for (const eventId of hookEventIds) {
|
|
630
|
+
delete state.events[eventId]
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
delete state.hooks[hookId]
|
|
634
|
+
delete state.seenHookActivityCounts[hookId]
|
|
635
|
+
|
|
636
|
+
if (state.activeHookId === hookId) {
|
|
637
|
+
state.activeHookId = null
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function upsertRun(
|
|
642
|
+
state: HookRegistryState,
|
|
643
|
+
event: RunLifecycleEvent,
|
|
644
|
+
eventId: string,
|
|
645
|
+
): void {
|
|
646
|
+
const existing = state.runs[event.runId]
|
|
647
|
+
if (!existing) {
|
|
648
|
+
state.runs[event.runId] = {
|
|
649
|
+
id: event.runId,
|
|
650
|
+
...(event.hookId ? { hookId: event.hookId } : {}),
|
|
651
|
+
...(event.threadId ? { threadId: event.threadId } : {}),
|
|
652
|
+
status: event.status,
|
|
653
|
+
...(event.source ? { source: event.source } : {}),
|
|
654
|
+
...(event.visibility ? { visibility: event.visibility } : {}),
|
|
655
|
+
startedAt: event.timestamp,
|
|
656
|
+
updatedAt: event.timestamp,
|
|
657
|
+
...(isTerminalRunStatus(event.status)
|
|
658
|
+
? { completedAt: event.timestamp }
|
|
659
|
+
: {}),
|
|
660
|
+
...(event.error ? { error: event.error } : {}),
|
|
661
|
+
eventIds: [eventId],
|
|
662
|
+
}
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (event.hookId) existing.hookId = event.hookId
|
|
667
|
+
if (event.threadId) existing.threadId = event.threadId
|
|
668
|
+
if (event.source) existing.source = event.source
|
|
669
|
+
if (event.visibility) existing.visibility = event.visibility
|
|
670
|
+
existing.status = event.status
|
|
671
|
+
existing.updatedAt = event.timestamp
|
|
672
|
+
if (isTerminalRunStatus(event.status)) {
|
|
673
|
+
existing.completedAt = event.timestamp
|
|
674
|
+
}
|
|
675
|
+
if (event.error) {
|
|
676
|
+
existing.error = event.error
|
|
677
|
+
}
|
|
678
|
+
if (!existing.eventIds.includes(eventId)) {
|
|
679
|
+
existing.eventIds.push(eventId)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function attachRunToHook(
|
|
684
|
+
state: HookRegistryState,
|
|
685
|
+
hookId: string,
|
|
686
|
+
runId: string,
|
|
687
|
+
): void {
|
|
688
|
+
const hook = state.hooks[hookId]
|
|
689
|
+
if (!hook || hook.runIds.includes(runId)) return
|
|
690
|
+
hook.runIds.push(runId)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function syncRunsFromSnapshot(
|
|
694
|
+
state: HookRegistryState,
|
|
695
|
+
snapshot: HookStateSnapshotEvent,
|
|
696
|
+
eventId: string,
|
|
697
|
+
): void {
|
|
698
|
+
const rawRuns = (snapshot.state as { runs?: unknown }).runs
|
|
699
|
+
if (!Array.isArray(rawRuns)) return
|
|
700
|
+
for (const candidate of rawRuns) {
|
|
701
|
+
if (!candidate || typeof candidate !== 'object') continue
|
|
702
|
+
const run = candidate as {
|
|
703
|
+
id?: unknown
|
|
704
|
+
status?: unknown
|
|
705
|
+
startedAt?: unknown
|
|
706
|
+
updatedAt?: unknown
|
|
707
|
+
completedAt?: unknown
|
|
708
|
+
error?: unknown
|
|
709
|
+
}
|
|
710
|
+
if (typeof run.id !== 'string') continue
|
|
711
|
+
const status = normalizeRunStatusFromSnapshot(run.status)
|
|
712
|
+
if (status === null) {
|
|
713
|
+
console.warn(
|
|
714
|
+
`[ai-devtools] unknown run.status in snapshot for hook "${snapshot.hookId}": ${String(run.status)}; skipping run "${run.id}"`,
|
|
715
|
+
)
|
|
716
|
+
continue
|
|
717
|
+
}
|
|
718
|
+
const startedAt =
|
|
719
|
+
typeof run.startedAt === 'number' ? run.startedAt : snapshot.timestamp
|
|
720
|
+
const updatedAt =
|
|
721
|
+
typeof run.updatedAt === 'number' ? run.updatedAt : startedAt
|
|
722
|
+
const existing = state.runs[run.id]
|
|
723
|
+
if (!existing) {
|
|
724
|
+
state.runs[run.id] = {
|
|
725
|
+
id: run.id,
|
|
726
|
+
hookId: snapshot.hookId,
|
|
727
|
+
status,
|
|
728
|
+
startedAt,
|
|
729
|
+
updatedAt,
|
|
730
|
+
...(typeof run.completedAt === 'number'
|
|
731
|
+
? { completedAt: run.completedAt }
|
|
732
|
+
: isTerminalRunStatus(status)
|
|
733
|
+
? { completedAt: updatedAt }
|
|
734
|
+
: {}),
|
|
735
|
+
...(typeof run.error === 'string' ? { error: run.error } : {}),
|
|
736
|
+
eventIds: [eventId],
|
|
737
|
+
}
|
|
738
|
+
} else {
|
|
739
|
+
existing.hookId = snapshot.hookId
|
|
740
|
+
existing.status = status
|
|
741
|
+
existing.updatedAt = updatedAt
|
|
742
|
+
if (typeof run.completedAt === 'number') {
|
|
743
|
+
existing.completedAt = run.completedAt
|
|
744
|
+
} else if (isTerminalRunStatus(status) && !existing.completedAt) {
|
|
745
|
+
existing.completedAt = updatedAt
|
|
746
|
+
}
|
|
747
|
+
if (typeof run.error === 'string') existing.error = run.error
|
|
748
|
+
if (!existing.eventIds.includes(eventId)) {
|
|
749
|
+
existing.eventIds.push(eventId)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
attachRunToHook(state, snapshot.hookId, run.id)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function normalizeRunStatusFromSnapshot(
|
|
757
|
+
value: unknown,
|
|
758
|
+
): RunLifecycleEvent['status'] | null {
|
|
759
|
+
switch (value) {
|
|
760
|
+
case 'created':
|
|
761
|
+
case 'started':
|
|
762
|
+
case 'updated':
|
|
763
|
+
case 'completed':
|
|
764
|
+
case 'errored':
|
|
765
|
+
case 'cancelled':
|
|
766
|
+
return value
|
|
767
|
+
case 'success':
|
|
768
|
+
return 'completed'
|
|
769
|
+
case 'error':
|
|
770
|
+
return 'errored'
|
|
771
|
+
case 'idle':
|
|
772
|
+
return 'created'
|
|
773
|
+
default:
|
|
774
|
+
return null
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function attachActivityRunToHook(
|
|
779
|
+
state: HookRegistryState,
|
|
780
|
+
hookId: string,
|
|
781
|
+
runId: string,
|
|
782
|
+
): void {
|
|
783
|
+
const hook = state.hooks[hookId]
|
|
784
|
+
if (!hook || hook.activityRunIds.includes(runId)) return
|
|
785
|
+
hook.activityRunIds.push(runId)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function attachEventToHook(
|
|
789
|
+
state: HookRegistryState,
|
|
790
|
+
hookId: string | undefined,
|
|
791
|
+
eventId: string,
|
|
792
|
+
): void {
|
|
793
|
+
if (!hookId) return
|
|
794
|
+
const hook = state.hooks[hookId]
|
|
795
|
+
if (!hook || hook.eventIds.includes(eventId)) return
|
|
796
|
+
hook.eventIds.push(eventId)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function inferLifecycleFromSnapshot(
|
|
800
|
+
state: Record<string, unknown>,
|
|
801
|
+
): HookLifecycle {
|
|
802
|
+
if (state.status === 'error' || state.error) {
|
|
803
|
+
return 'errored'
|
|
804
|
+
}
|
|
805
|
+
if (state.isLoading || state.status === 'generating') {
|
|
806
|
+
return 'streaming'
|
|
807
|
+
}
|
|
808
|
+
return 'active'
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function isTerminalRunStatus(status: RunLifecycleEvent['status']): boolean {
|
|
812
|
+
return (
|
|
813
|
+
status === 'completed' || status === 'errored' || status === 'cancelled'
|
|
814
|
+
)
|
|
815
|
+
}
|