@tanstack/ai-devtools-core 0.4.24 → 0.5.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/README.md +15 -1
- package/dist/Shell/{3ITTEC6O.js → DRZSZBFE.js} +706 -95
- package/dist/Shell/{54VAGOHT.js → G2SFOZV2.js} +513 -92
- package/dist/{production/Shell/SPYBPW3X.js → Shell/WD22ACIB.js} +706 -95
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/{3ITTEC6O.js → DRZSZBFE.js} +706 -95
- package/dist/production/Shell/{54VAGOHT.js → G2SFOZV2.js} +513 -92
- package/dist/{Shell/SPYBPW3X.js → production/Shell/WD22ACIB.js} +706 -95
- 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 +6 -6
- package/src/components/hooks/HookDashboard.tsx +2 -3
- package/src/components/hooks/HookDetails.tsx +37 -8
- package/src/components/hooks/MemoryPanel.tsx +296 -0
- package/src/components/hooks/hook-dashboard-model.ts +26 -0
- package/src/components/hooks/index.ts +1 -0
- package/src/store/ai-context.tsx +62 -0
- package/src/store/memory-registry.ts +228 -0
- package/src/styles/use-styles.ts +112 -0
package/dist/production/dev.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/
|
|
5
|
+
() => import('./Shell/WD22ACIB.js')
|
|
6
6
|
);
|
|
7
7
|
|
|
8
8
|
export { AiDevtoolsCore };
|
package/dist/production/index.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/
|
|
5
|
+
() => import('./Shell/DRZSZBFE.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/
|
|
5
|
+
() => import('./Shell/G2SFOZV2.js')
|
|
6
6
|
);
|
|
7
7
|
|
|
8
8
|
export { AiDevtoolsCore };
|
package/dist/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-devtools-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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,23 +84,23 @@
|
|
|
84
84
|
"@tanstack/devtools-utils": "^0.4.0",
|
|
85
85
|
"goober": "^2.1.18",
|
|
86
86
|
"solid-js": "^1.9.10",
|
|
87
|
-
"@tanstack/ai": "0.
|
|
88
|
-
"@tanstack/ai-event-client": "0.
|
|
87
|
+
"@tanstack/ai": "0.43.0",
|
|
88
|
+
"@tanstack/ai-event-client": "0.7.0"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@vitest/coverage-v8": "4.0.14",
|
|
92
92
|
"jsdom": "^27.2.0",
|
|
93
93
|
"tsup": "^8.5.1",
|
|
94
94
|
"tsup-preset-solid": "^2.2.0",
|
|
95
|
-
"vite": "^
|
|
95
|
+
"vite": "^8.1.4",
|
|
96
96
|
"vite-plugin-solid": "^2.11.10"
|
|
97
97
|
},
|
|
98
98
|
"scripts": {
|
|
99
99
|
"build": "tsup",
|
|
100
100
|
"clean": "premove ./build ./dist",
|
|
101
|
-
"lint:fix": "
|
|
101
|
+
"lint:fix": "oxlint src --type-aware --fix",
|
|
102
102
|
"test:build": "publint --strict",
|
|
103
|
-
"test:
|
|
103
|
+
"test:oxlint": "oxlint src --type-aware",
|
|
104
104
|
"test:lib": "vitest --passWithNoTests",
|
|
105
105
|
"test:lib:dev": "pnpm test:lib --watch",
|
|
106
106
|
"test:types": "tsc"
|
|
@@ -4,6 +4,7 @@ import { useStyles } from '../../styles/use-styles'
|
|
|
4
4
|
import { getHookUnseenEventCount } from '../../store/hook-registry'
|
|
5
5
|
import {
|
|
6
6
|
getHookDisplayName,
|
|
7
|
+
getHookIdentitySubtitle,
|
|
7
8
|
groupHooksByCategory,
|
|
8
9
|
isHookRunning,
|
|
9
10
|
visibleHooks,
|
|
@@ -244,9 +245,7 @@ export const HookDashboard: Component = () => {
|
|
|
244
245
|
</Show>
|
|
245
246
|
</div>
|
|
246
247
|
<span class={styles().hookDashboard.rowId}>
|
|
247
|
-
|
|
248
|
-
{hook.hookName} - {hook.id}
|
|
249
|
-
</Show>
|
|
248
|
+
{getHookIdentitySubtitle(hook)}
|
|
250
249
|
</span>
|
|
251
250
|
</div>
|
|
252
251
|
<div class={styles().hookDashboard.rowMeta}>
|
|
@@ -12,6 +12,8 @@ import { ToolFixtureForm } from './ToolFixtureForm'
|
|
|
12
12
|
import {
|
|
13
13
|
createHookDashboardSummary,
|
|
14
14
|
getHookDisplayName,
|
|
15
|
+
getHookIdentityLabel,
|
|
16
|
+
getHookIdentitySubtitle,
|
|
15
17
|
groupHooksByCategory,
|
|
16
18
|
isHookRunning,
|
|
17
19
|
visibleHooks,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
visiblePreviewPartsForMessage,
|
|
37
39
|
} from './preview-messages'
|
|
38
40
|
import { GenerationPanel, GenerationPreview } from './GenerationPanel'
|
|
41
|
+
import { MemoryPanel } from './MemoryPanel'
|
|
39
42
|
import type { HoverOrigin, HoverTarget, PreviewJsonItem } from './preview-model'
|
|
40
43
|
import type {
|
|
41
44
|
HookRecord,
|
|
@@ -46,7 +49,7 @@ import type {
|
|
|
46
49
|
import type { Conversation, Message, ToolCall } from '../../store/ai-store'
|
|
47
50
|
import type { Component, Setter } from 'solid-js'
|
|
48
51
|
|
|
49
|
-
type DetailTab = 'conversation' | 'tools' | 'state'
|
|
52
|
+
type DetailTab = 'conversation' | 'tools' | 'state' | 'memory'
|
|
50
53
|
type MessagePart = NonNullable<Message['parts']>[number]
|
|
51
54
|
const scrollAnimations = new WeakMap<HTMLElement, number>()
|
|
52
55
|
|
|
@@ -144,7 +147,12 @@ export const HookDetails: Component = () => {
|
|
|
144
147
|
})
|
|
145
148
|
|
|
146
149
|
createEffect(() => {
|
|
147
|
-
if
|
|
150
|
+
// Tools and Memory are chat-only tabs; if a generation hook becomes active
|
|
151
|
+
// while one of them is selected, fall back to the conversation view.
|
|
152
|
+
if (
|
|
153
|
+
isGenerationHook() &&
|
|
154
|
+
(activeTab() === 'tools' || activeTab() === 'memory')
|
|
155
|
+
) {
|
|
148
156
|
setActiveTab('conversation')
|
|
149
157
|
}
|
|
150
158
|
})
|
|
@@ -240,6 +248,14 @@ export const HookDetails: Component = () => {
|
|
|
240
248
|
activeTab={activeTab()}
|
|
241
249
|
onSelect={setActiveTab}
|
|
242
250
|
/>
|
|
251
|
+
<Show when={!isGenerationHook()}>
|
|
252
|
+
<TabButton
|
|
253
|
+
label="Memory"
|
|
254
|
+
tab="memory"
|
|
255
|
+
activeTab={activeTab()}
|
|
256
|
+
onSelect={setActiveTab}
|
|
257
|
+
/>
|
|
258
|
+
</Show>
|
|
243
259
|
</nav>
|
|
244
260
|
|
|
245
261
|
<div
|
|
@@ -283,6 +299,9 @@ export const HookDetails: Component = () => {
|
|
|
283
299
|
<Show when={activeTab() === 'state'}>
|
|
284
300
|
<JsonPanel value={activeHook().state} />
|
|
285
301
|
</Show>
|
|
302
|
+
<Show when={activeTab() === 'memory'}>
|
|
303
|
+
<MemoryPanel />
|
|
304
|
+
</Show>
|
|
286
305
|
</main>
|
|
287
306
|
|
|
288
307
|
<Show when={showSecondaryPane()}>
|
|
@@ -429,9 +448,7 @@ const HookOverview: Component<{
|
|
|
429
448
|
{getHookDisplayName(hook)}
|
|
430
449
|
</span>
|
|
431
450
|
<span class={styles().hookDetails.overviewHookId}>
|
|
432
|
-
|
|
433
|
-
{hook.hookName} - {hook.id}
|
|
434
|
-
</Show>
|
|
451
|
+
{getHookIdentitySubtitle(hook)}
|
|
435
452
|
</span>
|
|
436
453
|
</div>
|
|
437
454
|
<div class={styles().hookDetails.overviewHookMeta}>
|
|
@@ -513,9 +530,21 @@ const HookHeader: Component<{
|
|
|
513
530
|
{props.hook.hookName}
|
|
514
531
|
</span>
|
|
515
532
|
</Show>
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
533
|
+
{/* Prefer threadId as the user-facing identity (generation scope /
|
|
534
|
+
chat thread). Only show the raw registry id when it differs —
|
|
535
|
+
after the client prefers threadId over deprecated `id`, the two
|
|
536
|
+
often match and duplicating them is noise. */}
|
|
537
|
+
<span data-testid="ai-devtools-hook-identity">
|
|
538
|
+
{props.hook.threadId
|
|
539
|
+
? `thread ${props.hook.threadId}`
|
|
540
|
+
: getHookIdentityLabel(props.hook)}
|
|
541
|
+
</span>
|
|
542
|
+
<Show
|
|
543
|
+
when={props.hook.threadId && props.hook.threadId !== props.hook.id}
|
|
544
|
+
>
|
|
545
|
+
<span data-testid="ai-devtools-hook-registry-id">
|
|
546
|
+
id {props.hook.id}
|
|
547
|
+
</span>
|
|
519
548
|
</Show>
|
|
520
549
|
<Show when={props.hook.framework}>
|
|
521
550
|
<span>{props.hook.framework}</span>
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { For, Show, createMemo, createSignal } from 'solid-js'
|
|
2
|
+
import { useAIStore } from '../../store/ai-context'
|
|
3
|
+
import { useStyles } from '../../styles/use-styles'
|
|
4
|
+
import type { Component } from 'solid-js'
|
|
5
|
+
import { memoryScopeLabel } from '../../store/memory-registry'
|
|
6
|
+
import type {
|
|
7
|
+
MemoryEventRecord,
|
|
8
|
+
MemoryScopeState,
|
|
9
|
+
} from '../../store/memory-registry'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DevTools "Memory" tab. Memory is per composite scope (tenant/user/thread),
|
|
13
|
+
* not per-hook, so this panel reads the whole `state.memory` registry and lets
|
|
14
|
+
* the user pick a scope (defaulting to the most recently active). It renders
|
|
15
|
+
* two things:
|
|
16
|
+
* 1. Live contents — the latest `inspect()` records + `listFacts()` facts,
|
|
17
|
+
* pushed via `memory:snapshot` (only for adapters that support inspection).
|
|
18
|
+
* 2. Operations timeline — the `memory:*` recall/save/error events (always
|
|
19
|
+
* available, even when the adapter has no introspection).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Shape of a record inside the built-in adapters' `inspect()` payload. */
|
|
23
|
+
interface MemoryRecordRow {
|
|
24
|
+
id: string
|
|
25
|
+
text: string
|
|
26
|
+
kind: string
|
|
27
|
+
role?: string
|
|
28
|
+
createdAt?: number
|
|
29
|
+
importance?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Best-effort extraction of `{ records: [...] }` from the opaque snapshot data. */
|
|
33
|
+
function extractRecords(data: unknown): Array<MemoryRecordRow> {
|
|
34
|
+
if (!data || typeof data !== 'object') return []
|
|
35
|
+
const records = (data as { records?: unknown }).records
|
|
36
|
+
if (!Array.isArray(records)) return []
|
|
37
|
+
return records.filter(
|
|
38
|
+
(r): r is MemoryRecordRow =>
|
|
39
|
+
Boolean(r) &&
|
|
40
|
+
typeof r === 'object' &&
|
|
41
|
+
typeof (r as MemoryRecordRow).id === 'string' &&
|
|
42
|
+
typeof (r as MemoryRecordRow).text === 'string',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTime(value: number | string | undefined): string {
|
|
47
|
+
if (value === undefined) return ''
|
|
48
|
+
const date = new Date(value)
|
|
49
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
50
|
+
return date.toLocaleTimeString()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function eventSummary(event: MemoryEventRecord): string {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case 'retrieve:started':
|
|
56
|
+
return `recall "${event.query ?? ''}"`
|
|
57
|
+
case 'retrieve:completed':
|
|
58
|
+
return `recalled ${event.fragmentCount ?? 0} fragment(s), ${
|
|
59
|
+
event.systemPromptChars ?? 0
|
|
60
|
+
} prompt chars${event.hasTools ? ', +tools' : ''} (${event.durationMs ?? 0}ms)`
|
|
61
|
+
case 'persist:started':
|
|
62
|
+
return 'save started'
|
|
63
|
+
case 'persist:completed':
|
|
64
|
+
return `saved ${event.okCount ?? 0}/${event.receiptCount ?? 0} receipt(s) (${
|
|
65
|
+
event.durationMs ?? 0
|
|
66
|
+
}ms)`
|
|
67
|
+
case 'error':
|
|
68
|
+
return `${event.phase ?? ''} error: ${event.error?.message ?? 'unknown'}`
|
|
69
|
+
default:
|
|
70
|
+
return event.type
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const MemoryPanel: Component = () => {
|
|
75
|
+
const { state, clearMemory } = useAIStore()
|
|
76
|
+
const styles = useStyles()
|
|
77
|
+
const [override, setOverride] = createSignal<string | null>(null)
|
|
78
|
+
|
|
79
|
+
// Scope keys sorted most-recently-active first.
|
|
80
|
+
const scopeKeys = createMemo(() =>
|
|
81
|
+
Object.values(state.memory.scopes)
|
|
82
|
+
.slice()
|
|
83
|
+
.sort((a, b) => b.lastActivity - a.lastActivity)
|
|
84
|
+
.map((s) => s.key),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const selectedKey = createMemo(() => {
|
|
88
|
+
const chosen = override()
|
|
89
|
+
if (chosen && state.memory.scopes[chosen]) return chosen
|
|
90
|
+
return scopeKeys()[0] ?? null
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const scope = createMemo((): MemoryScopeState | undefined => {
|
|
94
|
+
const key = selectedKey()
|
|
95
|
+
return key ? state.memory.scopes[key] : undefined
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const records = createMemo(() => extractRecords(scope()?.snapshot?.data))
|
|
99
|
+
const facts = createMemo(() => scope()?.snapshot?.facts ?? [])
|
|
100
|
+
// Timeline newest-first.
|
|
101
|
+
const events = createMemo(() =>
|
|
102
|
+
(scope()?.events ?? []).slice().sort((a, b) => b.timestamp - a.timestamp),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
class={styles().memoryPanel.container}
|
|
108
|
+
data-testid="ai-devtools-memory-panel"
|
|
109
|
+
>
|
|
110
|
+
<Show
|
|
111
|
+
when={scope()}
|
|
112
|
+
fallback={
|
|
113
|
+
<div class={styles().memoryPanel.empty}>
|
|
114
|
+
No memory activity yet. Send a message through a chat wired with
|
|
115
|
+
<code> memoryMiddleware</code> and recall/save events will appear
|
|
116
|
+
here.
|
|
117
|
+
</div>
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
{(activeScope) => (
|
|
121
|
+
<>
|
|
122
|
+
<div class={styles().memoryPanel.toolbar}>
|
|
123
|
+
<div class={styles().memoryPanel.scopeControls}>
|
|
124
|
+
<Show
|
|
125
|
+
when={scopeKeys().length > 1}
|
|
126
|
+
fallback={
|
|
127
|
+
<span class={styles().memoryPanel.badge}>
|
|
128
|
+
{activeScope().adapter ?? 'memory'}
|
|
129
|
+
</span>
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<select
|
|
133
|
+
class={styles().memoryPanel.scopeSelect}
|
|
134
|
+
value={selectedKey() ?? ''}
|
|
135
|
+
onChange={(e) => setOverride(e.currentTarget.value)}
|
|
136
|
+
data-testid="ai-devtools-memory-scope-select"
|
|
137
|
+
>
|
|
138
|
+
<For each={scopeKeys()}>
|
|
139
|
+
{(key) => {
|
|
140
|
+
const entry = state.memory.scopes[key]
|
|
141
|
+
const label = entry ? memoryScopeLabel(entry) : key
|
|
142
|
+
return (
|
|
143
|
+
<option value={key}>
|
|
144
|
+
{entry?.adapter ? `${entry.adapter} · ` : ''}
|
|
145
|
+
{label.length > 40
|
|
146
|
+
? `${label.slice(0, 40)}…`
|
|
147
|
+
: label}
|
|
148
|
+
</option>
|
|
149
|
+
)
|
|
150
|
+
}}
|
|
151
|
+
</For>
|
|
152
|
+
</select>
|
|
153
|
+
</Show>
|
|
154
|
+
</div>
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
class={styles().memoryPanel.clearButton}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
setOverride(null)
|
|
160
|
+
clearMemory()
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
Clear
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Live contents (inspect + listFacts) */}
|
|
168
|
+
<section class={styles().memoryPanel.section}>
|
|
169
|
+
<div class={styles().memoryPanel.sectionTitle}>
|
|
170
|
+
Stored records ({records().length})
|
|
171
|
+
<Show when={activeScope().snapshot}>
|
|
172
|
+
{(snap) => (
|
|
173
|
+
<span class={styles().memoryPanel.time}>
|
|
174
|
+
snapshot {formatTime(snap().takenAt)}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</Show>
|
|
178
|
+
</div>
|
|
179
|
+
<Show
|
|
180
|
+
when={records().length > 0}
|
|
181
|
+
fallback={
|
|
182
|
+
<div class={styles().memoryPanel.sectionEmpty}>
|
|
183
|
+
{activeScope().snapshot
|
|
184
|
+
? 'Snapshot is empty.'
|
|
185
|
+
: 'This adapter does not expose inspect() — see the timeline below for activity.'}
|
|
186
|
+
</div>
|
|
187
|
+
}
|
|
188
|
+
>
|
|
189
|
+
<div class={styles().memoryPanel.list}>
|
|
190
|
+
<For each={records()}>
|
|
191
|
+
{(rec) => (
|
|
192
|
+
<div
|
|
193
|
+
class={styles().memoryPanel.row}
|
|
194
|
+
data-testid="ai-devtools-memory-record"
|
|
195
|
+
>
|
|
196
|
+
<div class={styles().memoryPanel.rowHeader}>
|
|
197
|
+
<span class={styles().memoryPanel.badge}>
|
|
198
|
+
{rec.kind}
|
|
199
|
+
</span>
|
|
200
|
+
<Show when={rec.role}>
|
|
201
|
+
<span>{rec.role}</span>
|
|
202
|
+
</Show>
|
|
203
|
+
<Show when={rec.importance !== undefined}>
|
|
204
|
+
<span>importance {rec.importance?.toFixed(2)}</span>
|
|
205
|
+
</Show>
|
|
206
|
+
<span class={styles().memoryPanel.time}>
|
|
207
|
+
{formatTime(rec.createdAt)}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
<div class={styles().memoryPanel.rowText}>
|
|
211
|
+
{rec.text}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</For>
|
|
216
|
+
</div>
|
|
217
|
+
</Show>
|
|
218
|
+
</section>
|
|
219
|
+
|
|
220
|
+
{/* Facts */}
|
|
221
|
+
<section class={styles().memoryPanel.section}>
|
|
222
|
+
<div class={styles().memoryPanel.sectionTitle}>
|
|
223
|
+
listFacts() ({facts().length})
|
|
224
|
+
</div>
|
|
225
|
+
<Show
|
|
226
|
+
when={facts().length > 0}
|
|
227
|
+
fallback={
|
|
228
|
+
<div class={styles().memoryPanel.sectionEmpty}>No facts.</div>
|
|
229
|
+
}
|
|
230
|
+
>
|
|
231
|
+
<div class={styles().memoryPanel.list}>
|
|
232
|
+
<For each={facts()}>
|
|
233
|
+
{(fact) => (
|
|
234
|
+
<div class={styles().memoryPanel.row}>
|
|
235
|
+
<div class={styles().memoryPanel.rowText}>
|
|
236
|
+
<Show when={fact.source}>
|
|
237
|
+
<span class={styles().memoryPanel.rowHeader}>
|
|
238
|
+
{fact.source}:{' '}
|
|
239
|
+
</span>
|
|
240
|
+
</Show>
|
|
241
|
+
{fact.text}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</For>
|
|
246
|
+
</div>
|
|
247
|
+
</Show>
|
|
248
|
+
</section>
|
|
249
|
+
|
|
250
|
+
{/* Operations timeline */}
|
|
251
|
+
<section class={styles().memoryPanel.section}>
|
|
252
|
+
<div class={styles().memoryPanel.sectionTitle}>
|
|
253
|
+
Operations ({events().length})
|
|
254
|
+
</div>
|
|
255
|
+
<Show
|
|
256
|
+
when={events().length > 0}
|
|
257
|
+
fallback={
|
|
258
|
+
<div class={styles().memoryPanel.sectionEmpty}>
|
|
259
|
+
No operations recorded.
|
|
260
|
+
</div>
|
|
261
|
+
}
|
|
262
|
+
>
|
|
263
|
+
<div class={styles().memoryPanel.list}>
|
|
264
|
+
<For each={events()}>
|
|
265
|
+
{(event) => (
|
|
266
|
+
<div
|
|
267
|
+
class={`${styles().memoryPanel.row} ${
|
|
268
|
+
event.type === 'error'
|
|
269
|
+
? styles().memoryPanel.rowError
|
|
270
|
+
: ''
|
|
271
|
+
}`}
|
|
272
|
+
data-testid="ai-devtools-memory-event"
|
|
273
|
+
>
|
|
274
|
+
<div class={styles().memoryPanel.rowHeader}>
|
|
275
|
+
<span class={styles().memoryPanel.badge}>
|
|
276
|
+
{event.type}
|
|
277
|
+
</span>
|
|
278
|
+
<span class={styles().memoryPanel.time}>
|
|
279
|
+
{formatTime(event.timestamp)}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
<div class={styles().memoryPanel.rowText}>
|
|
283
|
+
{eventSummary(event)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</For>
|
|
288
|
+
</div>
|
|
289
|
+
</Show>
|
|
290
|
+
</section>
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</Show>
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
@@ -61,6 +61,32 @@ export function getHookDisplayName(hook: HookRecord): string {
|
|
|
61
61
|
return hook.displayName ?? hook.hookName
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Stable identity string shown under the hook title.
|
|
66
|
+
*
|
|
67
|
+
* Prefer `threadId` when present — for generation hooks that is the single
|
|
68
|
+
* app-chosen scope (and, after the client prefers `threadId` over deprecated
|
|
69
|
+
* `id`, often equals the registry key too). Fall back to the registry `id`
|
|
70
|
+
* (devtools hook id / legacy client id) for ephemeral hooks that never named
|
|
71
|
+
* a thread.
|
|
72
|
+
*/
|
|
73
|
+
export function getHookIdentityLabel(hook: HookRecord): string {
|
|
74
|
+
return hook.threadId ?? hook.id
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Secondary subtitle under the title: either just the identity, or
|
|
79
|
+
* `hookName - identity` when a custom display name is set so the technical
|
|
80
|
+
* name stays visible.
|
|
81
|
+
*/
|
|
82
|
+
export function getHookIdentitySubtitle(hook: HookRecord): string {
|
|
83
|
+
const identity = getHookIdentityLabel(hook)
|
|
84
|
+
if (hook.displayName) {
|
|
85
|
+
return `${hook.hookName} - ${identity}`
|
|
86
|
+
}
|
|
87
|
+
return identity
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
export function groupHooksByCategory(
|
|
65
91
|
hooks: Array<HookRecord>,
|
|
66
92
|
): Array<HookCategoryGroup> {
|
package/src/store/ai-context.tsx
CHANGED
|
@@ -14,12 +14,19 @@ import {
|
|
|
14
14
|
createClientToolCallMessage,
|
|
15
15
|
shouldSkipClientAssistantPlaceholder,
|
|
16
16
|
} from './message-event-utils'
|
|
17
|
+
import {
|
|
18
|
+
applyMemoryEvent,
|
|
19
|
+
applyMemorySnapshot,
|
|
20
|
+
clearMemoryRegistry,
|
|
21
|
+
createMemoryRegistryState,
|
|
22
|
+
} from './memory-registry'
|
|
17
23
|
import type { ContentPartSource, TokenUsage } from '@tanstack/ai'
|
|
18
24
|
import type {
|
|
19
25
|
DevtoolsToolFixtureApplyEvent,
|
|
20
26
|
RunLifecycleEvent,
|
|
21
27
|
} from '@tanstack/ai-event-client'
|
|
22
28
|
import type { HookRegistryState, ToolFixtureRecord } from './hook-registry'
|
|
29
|
+
import type { MemoryRegistryState } from './memory-registry'
|
|
23
30
|
import type { ParentComponent } from 'solid-js'
|
|
24
31
|
|
|
25
32
|
interface MessagePart {
|
|
@@ -227,6 +234,7 @@ interface AIStoreState {
|
|
|
227
234
|
conversations: Record<string, Conversation>
|
|
228
235
|
activeConversationId: string | null
|
|
229
236
|
hooks: HookRegistryState
|
|
237
|
+
memory: MemoryRegistryState
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
interface AIContextValue {
|
|
@@ -234,6 +242,7 @@ interface AIContextValue {
|
|
|
234
242
|
clearAllConversations: () => void
|
|
235
243
|
selectConversation: (id: string) => void
|
|
236
244
|
clearHooks: () => void
|
|
245
|
+
clearMemory: () => void
|
|
237
246
|
selectHook: (id: string | null) => void
|
|
238
247
|
saveToolFixture: (fixture: ToolFixtureRecord) => void
|
|
239
248
|
deleteToolFixture: (fixtureId: string) => void
|
|
@@ -255,6 +264,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
255
264
|
conversations: {},
|
|
256
265
|
activeConversationId: null,
|
|
257
266
|
hooks: createHookRegistryState(),
|
|
267
|
+
memory: createMemoryRegistryState(),
|
|
258
268
|
})
|
|
259
269
|
|
|
260
270
|
const streamToConversation = new Map<string, string>()
|
|
@@ -641,6 +651,15 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
641
651
|
)
|
|
642
652
|
}
|
|
643
653
|
|
|
654
|
+
function clearMemory() {
|
|
655
|
+
setState(
|
|
656
|
+
'memory',
|
|
657
|
+
produce((memory: MemoryRegistryState) => {
|
|
658
|
+
clearMemoryRegistry(memory)
|
|
659
|
+
}),
|
|
660
|
+
)
|
|
661
|
+
}
|
|
662
|
+
|
|
644
663
|
function selectHook(id: string | null) {
|
|
645
664
|
setState(
|
|
646
665
|
'hooks',
|
|
@@ -1188,6 +1207,48 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
1188
1207
|
}),
|
|
1189
1208
|
)
|
|
1190
1209
|
|
|
1210
|
+
// Memory: the 5 `memory:*` operation events feed the timeline; `memory:snapshot`
|
|
1211
|
+
// replaces the per-scope stored-state view. Keyed by composite scope
|
|
1212
|
+
// (tenantId/userId/threadId).
|
|
1213
|
+
type MemoryEventInput = Parameters<typeof applyMemoryEvent>[1]
|
|
1214
|
+
const recordMemoryEvent = (
|
|
1215
|
+
type: MemoryEventInput['type'],
|
|
1216
|
+
payload: object,
|
|
1217
|
+
) => {
|
|
1218
|
+
setState(
|
|
1219
|
+
'memory',
|
|
1220
|
+
produce((memory: MemoryRegistryState) => {
|
|
1221
|
+
applyMemoryEvent(memory, { ...payload, type } as MemoryEventInput)
|
|
1222
|
+
}),
|
|
1223
|
+
)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
cleanupFns.push(
|
|
1227
|
+
aiEventClient.on('memory:retrieve:started', (e) => {
|
|
1228
|
+
recordMemoryEvent('retrieve:started', e.payload)
|
|
1229
|
+
}),
|
|
1230
|
+
aiEventClient.on('memory:retrieve:completed', (e) => {
|
|
1231
|
+
recordMemoryEvent('retrieve:completed', e.payload)
|
|
1232
|
+
}),
|
|
1233
|
+
aiEventClient.on('memory:persist:started', (e) => {
|
|
1234
|
+
recordMemoryEvent('persist:started', e.payload)
|
|
1235
|
+
}),
|
|
1236
|
+
aiEventClient.on('memory:persist:completed', (e) => {
|
|
1237
|
+
recordMemoryEvent('persist:completed', e.payload)
|
|
1238
|
+
}),
|
|
1239
|
+
aiEventClient.on('memory:error', (e) => {
|
|
1240
|
+
recordMemoryEvent('error', e.payload)
|
|
1241
|
+
}),
|
|
1242
|
+
aiEventClient.on('memory:snapshot', (e) => {
|
|
1243
|
+
setState(
|
|
1244
|
+
'memory',
|
|
1245
|
+
produce((memory: MemoryRegistryState) => {
|
|
1246
|
+
applyMemorySnapshot(memory, e.payload)
|
|
1247
|
+
}),
|
|
1248
|
+
)
|
|
1249
|
+
}),
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1191
1252
|
const recordRunEvent = (
|
|
1192
1253
|
eventName:
|
|
1193
1254
|
| 'run:created'
|
|
@@ -3402,6 +3463,7 @@ export const AIProvider: ParentComponent = (props) => {
|
|
|
3402
3463
|
clearAllConversations,
|
|
3403
3464
|
selectConversation,
|
|
3404
3465
|
clearHooks,
|
|
3466
|
+
clearMemory,
|
|
3405
3467
|
selectHook,
|
|
3406
3468
|
saveToolFixture,
|
|
3407
3469
|
deleteToolFixture,
|