@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.
- package/dist/Shell/{G2SFOZV2.js → DCUFLPG6.js} +518 -71
- package/dist/{production/Shell/WD22ACIB.js → Shell/JT2B6WKX.js} +774 -155
- package/dist/Shell/{DRZSZBFE.js → YBXRSRRK.js} +774 -155
- package/dist/dev.js +1 -1
- package/dist/index.js +1 -1
- package/dist/production/Shell/{G2SFOZV2.js → DCUFLPG6.js} +518 -71
- package/dist/{Shell/WD22ACIB.js → production/Shell/JT2B6WKX.js} +774 -155
- package/dist/production/Shell/{DRZSZBFE.js → YBXRSRRK.js} +774 -155
- 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 +3 -3
- package/src/components/conversation/IterationCard.tsx +11 -5
- package/src/components/hooks/CompactionPanel.tsx +223 -0
- package/src/components/hooks/HookDashboard.tsx +3 -2
- package/src/components/hooks/HookDetails.tsx +44 -6
- package/src/components/hooks/SkillsPanel.tsx +125 -0
- package/src/components/hooks/index.ts +2 -0
- package/src/store/ai-context.tsx +138 -5
- package/src/store/compaction-registry.ts +121 -0
- package/src/store/skills-registry.ts +50 -0
- package/src/styles/use-styles.ts +2 -3
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/JT2B6WKX.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/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/
|
|
5
|
+
() => import('./Shell/DCUFLPG6.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.5.
|
|
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.
|
|
88
|
-
"@tanstack/ai-event-client": "^0.11.
|
|
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
|
|
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
|
-
|
|
59
|
-
|
|
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 =
|
|
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
|
-
|
|
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' ||
|
|
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 (
|
|
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
|
-
|
|
208
|
-
|
|
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'
|