Skip to content

Commit db8cae4

Browse files
committed
feat(plugin-inspect): side-effectless components, storybook, styling, and dev server fix
Refactor components to be pure (View) vs smart (Smart), set up Storybook with viteFinal config, mirror Rolldown design language, match Devframe's brand color #517158, and replace SVGs with UnoCSS preset icons. Also adds a guard to the distDir assignment to prevent the devframe dev server from crashing when the SPA isn't built yet, and configures the vite plugin properly in the SPA's vite config to start the backend dev server.
1 parent 8ce1760 commit db8cae4

26 files changed

Lines changed: 2392 additions & 226 deletions

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ playwright-report
2121
playwright/.cache
2222
blob-report
2323
.ecosystem
24+
storybook-static

‎plugins/inspect/.storybook/main.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { StorybookConfig } from '@storybook/vue3-vite';
2+
import { mergeConfig } from 'vite';
3+
import vue from '@vitejs/plugin-vue';
4+
import UnoCSS from 'unocss/vite';
5+
import { alias } from '../../../alias';
6+
7+
const config: StorybookConfig = {
8+
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
9+
addons: ['@storybook/addon-essentials'],
10+
framework: {
11+
name: '@storybook/vue3-vite',
12+
options: {},
13+
},
14+
docs: {},
15+
async viteFinal(config) {
16+
return mergeConfig(config, {
17+
resolve: { alias },
18+
plugins: [vue(), UnoCSS()],
19+
});
20+
},
21+
};
22+
export default config;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { Preview } from '@storybook/vue3';
2+
import 'virtual:uno.css'
3+
import '../src/spa/style.css'
4+
5+
const preview: Preview = {
6+
parameters: {
7+
controls: {
8+
matchers: {
9+
color: /(background|color)$/i,
10+
date: /Date$/i,
11+
},
12+
},
13+
backgrounds: {
14+
default: 'dark',
15+
values: [
16+
{
17+
name: 'dark',
18+
value: '#111111',
19+
},
20+
{
21+
name: 'light',
22+
value: '#ffffff',
23+
},
24+
],
25+
},
26+
},
27+
};
28+
29+
export default preview;

‎plugins/inspect/package.json‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"watch": "tsdown --watch",
4343
"cli:build": "node bin.mjs build --out-dir dist/static",
4444
"prepack": "pnpm build",
45+
"storybook": "storybook dev -p 6006",
46+
"build-storybook": "storybook build",
4547
"test": "vitest run"
4648
},
4749
"peerDependencies": {
@@ -58,11 +60,18 @@
5860
"nostics": "catalog:deps"
5961
},
6062
"devDependencies": {
63+
"@iconify-json/ph": "catalog:",
64+
"@storybook/addon-essentials": "catalog:",
65+
"@storybook/vue3": "catalog:",
66+
"@storybook/vue3-vite": "catalog:",
67+
"@unocss/preset-icons": "catalog:",
6168
"@vitejs/plugin-vue": "catalog:build",
6269
"devframe": "workspace:*",
6370
"get-port-please": "catalog:deps",
6471
"h3": "catalog:deps",
72+
"storybook": "catalog:",
6573
"tsdown": "catalog:build",
74+
"unocss": "catalog:",
6675
"vite": "catalog:build",
6776
"vitest": "catalog:testing",
6877
"vue": "catalog:frontend",

‎plugins/inspect/src/index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { DevframeDefinition } from 'devframe/types'
2+
import { existsSync } from 'node:fs'
23
import { fileURLToPath } from 'node:url'
34
import { defineDevframe } from 'devframe/types'
45
import { setupInspect } from './node/index'
@@ -48,7 +49,7 @@ export function createInspectDevframe(options: InspectDevframeOptions = {}): Dev
4849
cli: {
4950
command: id,
5051
port: options.port ?? 9012,
51-
distDir,
52+
distDir: existsSync(distDir) ? distDir : undefined,
5253
// A single-user localhost inspector: skip the trust handshake so
5354
// the SPA's shared-state subscription initializes without a manual
5455
// auth round-trip. Hosted adapters (Vite/hub) supply their own

‎plugins/inspect/src/spa/App.vue‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<script setup lang="ts">
22
import { onMounted, ref } from 'vue'
3-
import AgentView from './components/AgentView.vue'
4-
import FunctionsView from './components/FunctionsView.vue'
5-
import StateView from './components/StateView.vue'
6-
import HistoryView from './components/HistoryView.vue'
3+
import AgentSmart from './components/AgentSmart.vue'
4+
import FunctionsSmart from './components/FunctionsSmart.vue'
5+
import StateSmart from './components/StateSmart.vue'
6+
import HistorySmart from './components/HistorySmart.vue'
77
import { connect, connection } from './composables/rpc'
88
import { useRefresh } from './composables/refresh'
99
@@ -62,9 +62,7 @@ onMounted(connect)
6262
:disabled="loading || !connection.connected"
6363
@click="refresh"
6464
>
65-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
66-
<path d="M21 12a9 9 0 1 1-2.64-6.36" /><path d="M21 3v6h-6" />
67-
</svg>
65+
<div class="i-ph-arrows-clockwise-duotone" />
6866
</button>
6967
</header>
7068

@@ -77,10 +75,10 @@ onMounted(connect)
7775
Connecting to devframe…
7876
</div>
7977
<template v-else>
80-
<FunctionsView v-if="tab === 'functions'" />
81-
<StateView v-else-if="tab === 'state'" />
82-
<AgentView v-else-if="tab === 'agent'" />
83-
<HistoryView v-else-if="tab === 'history'" />
78+
<FunctionsSmart v-if="tab === 'functions'" />
79+
<StateSmart v-else-if="tab === 'state'" />
80+
<AgentSmart v-else-if="tab === 'agent'" />
81+
<HistorySmart v-else-if="tab === 'history'" />
8482
</template>
8583
</main>
8684
</div>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script setup lang="ts">
2+
import type { AgentManifest, InvokeResult } from '@devframes/plugin-inspect/client'
3+
import { onMounted, reactive, shallowRef } from 'vue'
4+
import { isStatic, useRpc } from '../composables/rpc'
5+
import { useRefreshProvider } from '../composables/refresh'
6+
import AgentView from './AgentView.vue'
7+
8+
const rpc = useRpc()
9+
const manifest = shallowRef<AgentManifest | null>(null)
10+
const results = reactive<Record<string, InvokeResult | { ok: false, error: { name: string, message: string } }>>({})
11+
const pending = reactive<Record<string, boolean>>({})
12+
13+
async function fetchData(): Promise<void> {
14+
if (!rpc.value)
15+
return
16+
manifest.value = await rpc.value.call('devframes-plugin-inspect:describe-agent')
17+
}
18+
19+
useRefreshProvider(fetchData)
20+
onMounted(fetchData)
21+
22+
async function onInvoke(id: string, parsedArgs: unknown) {
23+
if (!rpc.value) return
24+
pending[id] = true
25+
try {
26+
results[id] = await rpc.value.call('devframes-plugin-inspect:invoke-agent-tool', id, parsedArgs)
27+
} catch (e) {
28+
const err = e as Error
29+
results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } }
30+
} finally {
31+
pending[id] = false
32+
}
33+
}
34+
35+
async function onRead(id: string) {
36+
if (!rpc.value) return
37+
pending[id] = true
38+
try {
39+
results[id] = await rpc.value.call('devframes-plugin-inspect:read-agent-resource', id)
40+
} catch (e) {
41+
const err = e as Error
42+
results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } }
43+
} finally {
44+
pending[id] = false
45+
}
46+
}
47+
</script>
48+
49+
<template>
50+
<AgentView
51+
:manifest="manifest"
52+
:is-static="isStatic()"
53+
:results="results"
54+
:pending="pending"
55+
@invoke="onInvoke"
56+
@read="onRead"
57+
/>
58+
</template>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Meta, StoryObj } from '@storybook/vue3';
2+
import AgentView from './AgentView.vue';
3+
4+
const meta = {
5+
title: 'Inspector/AgentView',
6+
component: AgentView,
7+
tags: ['autodocs'],
8+
argTypes: {
9+
onInvoke: { action: 'invoked' },
10+
onRead: { action: 'read' },
11+
},
12+
} satisfies Meta<typeof AgentView>;
13+
14+
export default meta;
15+
type Story = StoryObj<typeof meta>;
16+
17+
export const Default: Story = {
18+
args: {
19+
manifest: {
20+
tools: [
21+
{
22+
id: 'tool1',
23+
kind: 'rpc',
24+
title: 'Tool 1',
25+
description: 'A sample tool',
26+
safety: 'read',
27+
tags: ['sample'],
28+
inputSchema: { type: 'object', properties: { foo: { type: 'string' } } },
29+
outputSchema: { type: 'object', properties: { bar: { type: 'string' } } },
30+
},
31+
{
32+
id: 'tool2',
33+
kind: 'tool',
34+
title: 'Tool 2',
35+
description: 'A destructive tool',
36+
safety: 'destructive',
37+
}
38+
],
39+
resources: [
40+
{
41+
id: 'res1',
42+
uri: 'devframe://resource/res1',
43+
name: 'Resource 1',
44+
description: 'A sample resource',
45+
mimeType: 'application/json',
46+
}
47+
],
48+
},
49+
isStatic: false,
50+
results: {
51+
tool1: { ok: true, result: { bar: 'qux' }, durationMs: 15 },
52+
res1: { ok: false, error: { name: 'Error', message: 'Not found' }, durationMs: 2 },
53+
},
54+
pending: {
55+
tool2: true,
56+
},
57+
},
58+
};
59+
60+
export const StaticMode: Story = {
61+
args: {
62+
...Default.args,
63+
isStatic: true,
64+
},
65+
};

‎plugins/inspect/src/spa/components/AgentView.vue‎

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
<script setup lang="ts">
22
import type { AgentManifest, InvokeResult } from '@devframes/plugin-inspect/client'
3-
import { onMounted, reactive, ref, shallowRef } from 'vue'
4-
import { isStatic, useRpc } from '../composables/rpc'
5-
import { useRefreshProvider } from '../composables/refresh'
3+
import { ref, reactive } from 'vue'
64
import JsonView from './JsonView.vue'
75
8-
const rpc = useRpc()
9-
const manifest = shallowRef<AgentManifest | null>(null)
10-
const expanded = ref<string | null>(null)
11-
const argsInput = reactive<Record<string, string>>({})
12-
const results = reactive<Record<string, InvokeResult | { ok: false, error: { name: string, message: string } }>>({})
13-
const pending = reactive<Record<string, boolean>>({})
6+
const props = defineProps<{
7+
manifest: AgentManifest | null
8+
isStatic: boolean
9+
results: Record<string, InvokeResult | { ok: false, error: { name: string, message: string } }>
10+
pending: Record<string, boolean>
11+
}>()
1412
15-
async function fetchData(): Promise<void> {
16-
if (!rpc.value)
17-
return
18-
manifest.value = await rpc.value.call('devframes-plugin-inspect:describe-agent')
19-
}
13+
const emit = defineEmits<{
14+
(e: 'invoke', id: string, parsedArgs: unknown): void
15+
(e: 'read', id: string): void
16+
}>()
2017
21-
useRefreshProvider(fetchData)
22-
onMounted(fetchData)
18+
const expanded = ref<string | null>(null)
19+
const argsInput = reactive<Record<string, string>>({})
2320
2421
function toggle(id: string): void {
2522
expanded.value = expanded.value === id ? null : id
@@ -28,38 +25,20 @@ function toggle(id: string): void {
2825
}
2926
}
3027
31-
async function invokeTool(id: string) {
32-
if (!rpc.value) return
28+
function invokeTool(id: string) {
3329
let parsed: unknown
3430
try {
3531
const raw = JSON.parse(argsInput[id] || '{}')
3632
parsed = raw
37-
} catch (e) {
38-
results[id] = { ok: false, error: { name: 'SyntaxError', message: `Invalid JSON args: ${(e as Error).message}` } }
39-
return
40-
}
41-
pending[id] = true
42-
try {
43-
results[id] = await rpc.value.call('devframes-plugin-inspect:invoke-agent-tool', id, parsed)
44-
} catch (e) {
45-
const err = e as Error
46-
results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } }
47-
} finally {
48-
pending[id] = false
33+
} catch (err) {
34+
// Emitting error would be cleaner, for now we will throw to stop execution
35+
throw new Error(`Invalid JSON args: ${(err as Error).message}`)
4936
}
37+
emit('invoke', id, parsed)
5038
}
5139
52-
async function readResource(id: string) {
53-
if (!rpc.value) return
54-
pending[id] = true
55-
try {
56-
results[id] = await rpc.value.call('devframes-plugin-inspect:read-agent-resource', id)
57-
} catch (e) {
58-
const err = e as Error
59-
results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } }
60-
} finally {
61-
pending[id] = false
62-
}
40+
function readResource(id: string) {
41+
emit('read', id)
6342
}
6443
</script>
6544

@@ -78,7 +57,7 @@ async function readResource(id: string) {
7857
<div v-else class="cards">
7958
<div v-for="tool in manifest.tools" :key="tool.id" class="card">
8059
<div class="card-head" style="cursor: pointer; user-select: none;" @click="toggle(tool.id)">
81-
<svg class="chev" :class="{ open: expanded === tool.id }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6" /></svg>
60+
<div class="chev i-ph-caret-right" :class="{ open: expanded === tool.id }" />
8261
<span class="card-title">{{ tool.title }}</span>
8362
<span class="badge flag">{{ tool.kind }}</span>
8463
<span class="badge" :class="`safety-${tool.safety}`">{{ tool.safety }}</span>
@@ -106,13 +85,13 @@ async function readResource(id: string) {
10685
<JsonView :value="tool.examples" :expand-depth="1" />
10786
</template>
10887

109-
<div class="label">Invoke (JSON object)</div>
88+
<div class="label">Invoke (MCP format)</div>
11089
<textarea v-model="argsInput[tool.id]" class="args" spellcheck="false" placeholder="{}" />
11190
<div style="margin-top: 8px; display: flex; gap: 8px; align-items: center;">
112-
<button class="btn" :disabled="pending[tool.id] || isStatic()" @click="invokeTool(tool.id)">
91+
<button class="btn" :disabled="pending[tool.id] || isStatic" @click="invokeTool(tool.id)">
11392
{{ pending[tool.id] ? 'Invoking…' : 'Invoke' }}
11493
</button>
115-
<span v-if="isStatic()" class="note">read-only static backend</span>
94+
<span v-if="isStatic" class="note">read-only static backend</span>
11695
</div>
11796

11897
<div v-if="results[tool.id]" class="result">
@@ -141,7 +120,7 @@ async function readResource(id: string) {
141120
<div v-else class="cards">
142121
<div v-for="res in manifest.resources" :key="res.id" class="card">
143122
<div class="card-head" style="cursor: pointer; user-select: none;" @click="toggle(res.id)">
144-
<svg class="chev" :class="{ open: expanded === res.id }" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6" /></svg>
123+
<div class="chev i-ph-caret-right" :class="{ open: expanded === res.id }" />
145124
<span class="card-title">{{ res.name }}</span>
146125
<span v-if="res.mimeType" class="badge flag">{{ res.mimeType }}</span>
147126
</div>
@@ -154,10 +133,10 @@ async function readResource(id: string) {
154133

155134
<template v-if="expanded === res.id">
156135
<div style="margin-top: 8px; display: flex; gap: 8px; align-items: center;">
157-
<button class="btn" :disabled="pending[res.id] || isStatic()" @click="readResource(res.id)">
136+
<button class="btn" :disabled="pending[res.id] || isStatic" @click="readResource(res.id)">
158137
{{ pending[res.id] ? 'Reading…' : 'Read Resource' }}
159138
</button>
160-
<span v-if="isStatic()" class="note">read-only static backend</span>
139+
<span v-if="isStatic" class="note">read-only static backend</span>
161140
</div>
162141

163142
<div v-if="results[res.id]" class="result">

0 commit comments

Comments
 (0)