Skip to content

Commit 89d579c

Browse files
committed
feat(plugin-inspect): side-effectless components, storybook, styling
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.
1 parent d614d02 commit 89d579c

80 files changed

Lines changed: 60396 additions & 227 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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/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)