@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.
@@ -0,0 +1,228 @@
1
+ import type {
2
+ MemoryErrorEvent,
3
+ MemoryPersistCompletedEvent,
4
+ MemoryPersistStartedEvent,
5
+ MemoryRetrieveCompletedEvent,
6
+ MemoryRetrieveStartedEvent,
7
+ MemoryScopeLite,
8
+ MemorySnapshotEvent,
9
+ } from '@tanstack/ai-event-client'
10
+
11
+ /**
12
+ * DevTools-side accumulator for the `memory:*` event stream. Kept as a set of
13
+ * pure reducers (mirroring `hook-registry.ts`) so the mapping from events →
14
+ * view state is unit-testable in isolation, without a Solid store.
15
+ *
16
+ * Memory is keyed by composite scope (`tenantId`/`userId`/`threadId`), NOT by
17
+ * hook — several hooks can share one scope. The `MemoryPanel` reads a single
18
+ * `MemoryScopeState` by key; the per-hook tab just resolves which key to show.
19
+ */
20
+
21
+ /** One row in a scope's operations timeline. */
22
+ export interface MemoryEventRecord {
23
+ id: string
24
+ type:
25
+ | 'retrieve:started'
26
+ | 'retrieve:completed'
27
+ | 'persist:started'
28
+ | 'persist:completed'
29
+ | 'error'
30
+ timestamp: number
31
+ adapter: string
32
+ /** recall:started — the recall query (last user text). */
33
+ query?: string
34
+ /** recall:completed. */
35
+ fragmentCount?: number
36
+ hasTools?: boolean
37
+ systemPromptChars?: number
38
+ /** persist:completed. */
39
+ receiptCount?: number
40
+ okCount?: number
41
+ /** recall:completed / persist:completed. */
42
+ durationMs?: number
43
+ /** error. */
44
+ phase?: 'recall' | 'save'
45
+ error?: { name: string; message: string }
46
+ }
47
+
48
+ /** A flat fact row, mirroring `MemoryFact` from `@tanstack/ai-memory`. */
49
+ export interface MemoryFactRecord {
50
+ id: string
51
+ text: string
52
+ source?: string
53
+ createdAt?: string
54
+ }
55
+
56
+ /** Latest `inspect()` + `listFacts()` snapshot pushed via `memory:snapshot`. */
57
+ export interface MemorySnapshotRecord {
58
+ takenAt: string
59
+ data: unknown
60
+ facts: Array<MemoryFactRecord>
61
+ }
62
+
63
+ /** Everything known about memory for a single composite scope. */
64
+ export interface MemoryScopeState {
65
+ key: string
66
+ threadId: string
67
+ userId?: string
68
+ tenantId?: string
69
+ namespace?: string
70
+ /** Most recent adapter id seen for this scope. */
71
+ adapter?: string
72
+ events: Array<MemoryEventRecord>
73
+ snapshot?: MemorySnapshotRecord
74
+ lastActivity: number
75
+ }
76
+
77
+ export interface MemoryRegistryState {
78
+ scopes: Record<string, MemoryScopeState>
79
+ }
80
+
81
+ export function createMemoryRegistryState(): MemoryRegistryState {
82
+ return { scopes: {} }
83
+ }
84
+
85
+ /**
86
+ * Escape `:` / `\` / `_` so composite keys cannot collide when a dim contains
87
+ * the separator or the unset sentinel (mirrors redis scope-key hardening).
88
+ */
89
+ function escapeScopeDim(value: string): string {
90
+ return value.replace(/[\\:_]/g, '\\$&')
91
+ }
92
+
93
+ /** Unset optional dims serialize as `_` (same convention as the redis adapter). */
94
+ function scopeDim(value: string | undefined): string {
95
+ return value != null && value.length > 0 ? escapeScopeDim(value) : '_'
96
+ }
97
+
98
+ /**
99
+ * Stable scope key:
100
+ * `{tenantId|_}:{userId|_}:{threadId}:{namespace|_}`.
101
+ * Absent scope (e.g. `memory:error` before resolve) buckets to `(unknown)`.
102
+ */
103
+ export function memoryScopeKey(scope: MemoryScopeLite | undefined): string {
104
+ if (!scope) return '(unknown)'
105
+ return `${scopeDim(scope.tenantId)}:${scopeDim(scope.userId)}:${escapeScopeDim(scope.threadId)}:${scopeDim(scope.namespace)}`
106
+ }
107
+
108
+ /** Human-readable label for the Memory panel scope picker. */
109
+ export function memoryScopeLabel(entry: MemoryScopeState): string {
110
+ if (entry.key === '(unknown)' || !entry.threadId) return '(unknown)'
111
+ const parts = [
112
+ entry.tenantId,
113
+ entry.userId,
114
+ entry.threadId,
115
+ entry.namespace,
116
+ ].filter((p): p is string => p != null && p.length > 0)
117
+ return parts.join(' · ')
118
+ }
119
+
120
+ const MAX_EVENTS_PER_SCOPE = 200
121
+
122
+ function ensureScope(
123
+ state: MemoryRegistryState,
124
+ scope: MemoryScopeLite | undefined,
125
+ ): MemoryScopeState {
126
+ const key = memoryScopeKey(scope)
127
+ let entry = state.scopes[key]
128
+ if (!entry) {
129
+ entry = {
130
+ key,
131
+ threadId: scope?.threadId ?? '',
132
+ userId: scope?.userId,
133
+ tenantId: scope?.tenantId,
134
+ namespace: scope?.namespace,
135
+ events: [],
136
+ lastActivity: 0,
137
+ }
138
+ state.scopes[key] = entry
139
+ }
140
+ // Only merge metadata when we have a real scope (not the unscoped error bucket).
141
+ if (scope) {
142
+ if (scope.userId) entry.userId = scope.userId
143
+ if (scope.tenantId) entry.tenantId = scope.tenantId
144
+ if (scope.namespace) entry.namespace = scope.namespace
145
+ }
146
+ return entry
147
+ }
148
+
149
+ let fallbackCounter = 0
150
+
151
+ function eventId(
152
+ payload: { eventId?: string },
153
+ type: string,
154
+ ts: number,
155
+ ): string {
156
+ if (payload.eventId && payload.eventId.length > 0) return payload.eventId
157
+ return `${type}:${ts}:${fallbackCounter++}`
158
+ }
159
+
160
+ type MemoryEventPayload =
161
+ | ({ type: 'retrieve:started' } & MemoryRetrieveStartedEvent)
162
+ | ({ type: 'retrieve:completed' } & MemoryRetrieveCompletedEvent)
163
+ | ({ type: 'persist:started' } & MemoryPersistStartedEvent)
164
+ | ({ type: 'persist:completed' } & MemoryPersistCompletedEvent)
165
+ | ({ type: 'error' } & MemoryErrorEvent)
166
+
167
+ /** Append one `memory:*` operation event to its scope's timeline. */
168
+ export function applyMemoryEvent(
169
+ state: MemoryRegistryState,
170
+ event: MemoryEventPayload,
171
+ ): void {
172
+ const entry = ensureScope(state, event.scope)
173
+ entry.adapter = event.adapter
174
+ entry.lastActivity = Math.max(entry.lastActivity, event.timestamp)
175
+
176
+ const record: MemoryEventRecord = {
177
+ id: eventId(event, event.type, event.timestamp),
178
+ type: event.type,
179
+ timestamp: event.timestamp,
180
+ adapter: event.adapter,
181
+ }
182
+ switch (event.type) {
183
+ case 'retrieve:started':
184
+ record.query = event.query
185
+ break
186
+ case 'retrieve:completed':
187
+ record.fragmentCount = event.fragmentCount
188
+ record.hasTools = event.hasTools
189
+ record.systemPromptChars = event.systemPromptChars
190
+ record.durationMs = event.durationMs
191
+ break
192
+ case 'persist:completed':
193
+ record.receiptCount = event.receiptCount
194
+ record.okCount = event.okCount
195
+ record.durationMs = event.durationMs
196
+ break
197
+ case 'error':
198
+ record.phase = event.phase
199
+ record.error = event.error
200
+ break
201
+ case 'persist:started':
202
+ break
203
+ }
204
+
205
+ entry.events.push(record)
206
+ if (entry.events.length > MAX_EVENTS_PER_SCOPE) {
207
+ entry.events.splice(0, entry.events.length - MAX_EVENTS_PER_SCOPE)
208
+ }
209
+ }
210
+
211
+ /** Replace a scope's stored-state snapshot from a `memory:snapshot` event. */
212
+ export function applyMemorySnapshot(
213
+ state: MemoryRegistryState,
214
+ event: MemorySnapshotEvent,
215
+ ): void {
216
+ const entry = ensureScope(state, event.scope)
217
+ entry.adapter = event.adapter
218
+ entry.lastActivity = Math.max(entry.lastActivity, event.timestamp)
219
+ entry.snapshot = {
220
+ takenAt: event.takenAt,
221
+ data: event.data,
222
+ facts: event.facts,
223
+ }
224
+ }
225
+
226
+ export function clearMemoryRegistry(state: MemoryRegistryState): void {
227
+ state.scopes = {}
228
+ }
@@ -1898,6 +1898,118 @@ const stylesFactory = (theme: 'light' | 'dark') => {
1898
1898
  padding: 0 ${size[3]};
1899
1899
  `,
1900
1900
  },
1901
+ // MemoryPanel component styles
1902
+ memoryPanel: {
1903
+ container: css`
1904
+ display: flex;
1905
+ flex: 1;
1906
+ min-height: 0;
1907
+ flex-direction: column;
1908
+ gap: ${size[4]};
1909
+ overflow-y: auto;
1910
+ padding: ${size[4]};
1911
+ `,
1912
+ toolbar: css`
1913
+ display: flex;
1914
+ align-items: center;
1915
+ justify-content: space-between;
1916
+ gap: ${size[3]};
1917
+ `,
1918
+ scopeControls: css`
1919
+ display: flex;
1920
+ align-items: center;
1921
+ gap: ${size[2]};
1922
+ min-width: 0;
1923
+ `,
1924
+ scopeSelect: css`
1925
+ max-width: 220px;
1926
+ border: 1px solid ${t(colors.gray[200], colors.darkGray[700])};
1927
+ border-radius: ${border.radius.sm};
1928
+ background: ${t(colors.white, colors.darkGray[800])};
1929
+ color: ${t(colors.gray[900], colors.gray[100])};
1930
+ font-family: ${fontFamily.mono};
1931
+ font-size: ${fontSize.xs};
1932
+ padding: ${size[1]} ${size[2]};
1933
+ `,
1934
+ clearButton: css`
1935
+ border: 1px solid ${t(colors.gray[200], colors.darkGray[700])};
1936
+ border-radius: ${border.radius.sm};
1937
+ background: ${t(colors.gray[50], colors.darkGray[700])};
1938
+ color: ${t(colors.gray[700], colors.gray[200])};
1939
+ cursor: pointer;
1940
+ font-size: ${fontSize.xs};
1941
+ padding: ${size[1]} ${size[2]};
1942
+ &:hover {
1943
+ border-color: ${t(colors.blue[300], colors.blue[700])};
1944
+ }
1945
+ `,
1946
+ empty: css`
1947
+ color: ${t(colors.gray[500], colors.gray[500])};
1948
+ font-size: ${fontSize.sm};
1949
+ padding: ${size[4]};
1950
+ text-align: center;
1951
+ `,
1952
+ section: css`
1953
+ display: flex;
1954
+ flex-direction: column;
1955
+ gap: ${size[2]};
1956
+ `,
1957
+ sectionTitle: css`
1958
+ color: ${t(colors.gray[600], colors.gray[300])};
1959
+ font-size: ${fontSize.xs};
1960
+ font-weight: ${font.weight.semibold};
1961
+ text-transform: uppercase;
1962
+ letter-spacing: 0.04em;
1963
+ `,
1964
+ sectionEmpty: css`
1965
+ color: ${t(colors.gray[500], colors.gray[500])};
1966
+ font-size: ${fontSize.xs};
1967
+ `,
1968
+ list: css`
1969
+ display: flex;
1970
+ flex-direction: column;
1971
+ gap: ${size[1]};
1972
+ `,
1973
+ row: css`
1974
+ display: flex;
1975
+ flex-direction: column;
1976
+ gap: 2px;
1977
+ border: 1px solid ${t(colors.gray[200], colors.darkGray[700])};
1978
+ border-radius: ${border.radius.sm};
1979
+ background: ${t(colors.white, colors.darkGray[800])};
1980
+ padding: ${size[2]};
1981
+ `,
1982
+ rowError: css`
1983
+ border-color: ${t(colors.red[300], colors.red[700])};
1984
+ background: ${t(colors.red[50], colors.red[900] + '20')};
1985
+ `,
1986
+ rowHeader: css`
1987
+ display: flex;
1988
+ align-items: center;
1989
+ gap: ${size[2]};
1990
+ font-size: ${fontSize.xs};
1991
+ color: ${t(colors.gray[500], colors.gray[400])};
1992
+ `,
1993
+ badge: css`
1994
+ border-radius: ${border.radius.xs};
1995
+ background: ${t(colors.blue[50], colors.blue[900] + '30')};
1996
+ color: ${t(colors.blue[700], colors.blue[300])};
1997
+ font-family: ${fontFamily.mono};
1998
+ font-size: 10px;
1999
+ padding: 1px 6px;
2000
+ `,
2001
+ time: css`
2002
+ margin-left: auto;
2003
+ font-family: ${fontFamily.mono};
2004
+ font-size: 10px;
2005
+ `,
2006
+ rowText: css`
2007
+ color: ${t(colors.gray[900], colors.gray[100])};
2008
+ font-size: ${fontSize.sm};
2009
+ white-space: pre-wrap;
2010
+ word-break: break-word;
2011
+ `,
2012
+ },
1901
2013
  // ConversationDetails component styles
1902
2014
  conversationDetails: {
1903
2015
  emptyState: css`