@standardagents/vue
Vue 3 composables and plugin for Standard Agents Builder - connect to AI agent threads with real-time updates
Overview
The @standardagents/vue package provides Vue 3 composables and a plugin for building UIs that connect to Standard Agents Builder threads. It handles real-time message streaming, custom event listening, and workblock transformation for tool visualization.
- Real-time WebSocket updates for live message streaming
- Vue plugin with provide/inject architecture
- Workblock transformation for UI-friendly tool displays
- Custom event system for backend-to-frontend communication
- File uploads and attachments for sending images to the LLM
- TypeScript support with full type definitions
- Works with any Vue 3 framework (Nuxt, Vite, etc.)
Installation
npm
npm install @standardagents/vuepnpm
pnpm add @standardagents/vueyarn
yarn add @standardagents/vueRequirements: Vue 3.3+
Quick Start
// main.ts
import { createApp } from 'vue'
import { StandardAgentsPlugin } from '@standardagents/vue'
import App from './App.vue'
const app = createApp(App)
app.use(StandardAgentsPlugin, { endpoint: 'https://your-api.com/api' })
app.mount('#app')<!-- App.vue -->
<script setup lang="ts">
import { ThreadProvider } from '@standardagents/vue'
import ChatInterface from './ChatInterface.vue'
</script>
<template>
<ThreadProvider threadId="123e4567-e89b-12d3-a456-426614174000">
<ChatInterface />
</ThreadProvider>
</template><!-- ChatInterface.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useThread } from '@standardagents/vue'
const { messages, sendMessage, status } = useThread()
const input = ref('')
const handleSend = async () => {
if (input.value.trim()) {
await sendMessage({ role: 'user', content: input.value })
input.value = ''
}
}
</script>
<template>
<div>
<p>Status: {{ status }}</p>
<div v-for="msg in messages" :key="msg.id">
<strong>{{ msg.role }}:</strong> {{ msg.content }}
</div>
<input v-model="input" @keyup.enter="handleSend" />
<button @click="handleSend">Send</button>
</div>
</template>Plugin Setup
StandardAgentsPlugin
Install the plugin when creating your Vue application to configure the API endpoint.
import { createApp } from 'vue'
import { StandardAgentsPlugin } from '@standardagents/vue'
const app = createApp(App)
app.use(StandardAgentsPlugin, { endpoint: 'https://api.example.com/api' })Options:
| Option | Type | Required | Description |
|---|---|---|---|
endpoint |
string |
Yes | The API endpoint URL |
Components
ThreadProvider
Component that establishes a WebSocket connection to a thread and provides context to child components.
<script setup lang="ts">
import { ThreadProvider } from '@standardagents/vue'
</script>
<template>
<ThreadProvider
threadId="123e4567-e89b-12d3-a456-426614174000"
:preload="true"
:live="true"
:useWorkblocks="false"
:depth="0"
:includeSilent="false"
>
<ChatInterface />
</ThreadProvider>
</template>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
threadId |
string |
Required | The thread ID to connect to |
preload |
boolean |
true |
Fetch existing messages on mount |
live |
boolean |
true |
Enable WebSocket for live updates |
useWorkblocks |
boolean |
false |
Transform tool calls into workblocks |
useSubagentBlocks |
boolean |
false |
Transform messages into subagent blocks |
depth |
number |
0 |
Max message depth (0 = top-level only) |
includeSilent |
boolean |
false |
Include silent messages |
endpoint |
string |
- | Override endpoint from plugin |
Composables
useStandardAgents
Access the Standard Agents client directly for advanced operations.
<script setup lang="ts">
import { useStandardAgents } from '@standardagents/vue'
const { client, endpoint } = useStandardAgents()
// Create a new thread
const createNewThread = async () => {
const { threadId } = await client.createThread({ agent_id: 'my-agent' })
console.log('Created:', threadId)
}
</script>Returns:
| Property | Type | Description |
|---|---|---|
client |
AgentBuilderClient |
The HTTP/WebSocket client instance |
endpoint |
string |
The configured endpoint URL |
useThread
Composable to access the full thread context. Must be used within a ThreadProvider.
<script setup lang="ts">
import { useThread } from '@standardagents/vue'
const {
// Messages & State
threadId,
messages,
workblocks,
status,
runStatus,
loading,
error,
options,
// Actions
sendMessage,
stopExecution,
deleteMessage,
// Custom Events
onEvent,
// File Management (uploads to filesystem)
files,
addFiles,
removeFile,
getFileUrl,
getPreviewUrl,
// Attachment Management (sent to LLM)
attachments,
addAttachment,
removeAttachment,
clearAttachments,
} = useThread()
</script>Method Reference
sendMessage
Send a message to the thread with the pending attachments. The pending queue clears when sending starts. A failed send restores the queue for the same session.
<script setup lang="ts">
const { sendMessage, attachments } = useThread()
async function handleSend() {
// Simple text message
await sendMessage({ role: 'user', content: 'Hello!' })
// With attachments (call addAttachment first)
// Attachments are auto-included and cleared after send
await sendMessage({ role: 'user', content: 'What is in this image?' })
}
</script>Sending returns a queued acknowledgement. Its messageId identifies the accepted message. Read the message from thread state after its message_data frame arrives.
Signature:
sendMessage(payload: Omit<SendMessagePayload, 'attachments'>): Promise<SendMessageResponse>
interface SendMessageResponse {
status: 'queued'
messageId: string
}
interface SendMessagePayload {
role: 'user' | 'assistant' | 'system'
content: string
silent?: boolean
}Optimistic UI: When you call sendMessage(), an optimistic message with attachment previews is shown immediately. The real message replaces it when confirmed by the server via WebSocket.
addAttachment
Queue file(s) to be sent with the next message. Files are NOT uploaded immediately - they're held locally with preview URLs and sent inline (base64) when sendMessage() is called.
Use this for images/files that should be sent directly to the LLM.
<script setup lang="ts">
const { addAttachment, attachments, removeAttachment } = useThread()
function handleFileSelect(event: Event) {
const input = event.target as HTMLInputElement
if (input.files?.length) {
addAttachment(input.files)
}
}
</script>
<template>
<input type="file" @change="handleFileSelect" multiple />
<!-- Show previews for pending attachments -->
<div v-for="a in attachments" :key="a.id">
<img v-if="a.isImage && a.previewUrl" :src="a.previewUrl" :alt="a.name" />
<span>{{ a.name }}</span>
<button @click="removeAttachment(a.id)">Remove</button>
</div>
</template>Signature:
addAttachment(files: File | File[] | FileList): voidPendingAttachment Type:
interface PendingAttachment {
id: string // Temporary ID
file: File // Original file object
name: string
mimeType: string
size: number
isImage: boolean
previewUrl: string | null // Object URL for image preview
width?: number
height?: number
}removeAttachment
Remove a pending attachment before sending.
<script setup lang="ts">
const { removeAttachment, attachments } = useThread()
</script>
<template>
<div v-for="a in attachments" :key="a.id">
<span>{{ a.name }}</span>
<button @click="removeAttachment(a.id)">Remove</button>
</div>
</template>Signature:
removeAttachment(id: string): voidclearAttachments
Remove all pending attachments.
<script setup lang="ts">
const { clearAttachments, attachments } = useThread()
</script>
<template>
<button v-if="attachments.length" @click="clearAttachments">
Clear All
</button>
</template>Signature:
clearAttachments(): voidaddFiles
Upload files to the thread's filesystem. Files are uploaded immediately and stored persistently.
Use this for documents/files that should be stored on the thread but NOT sent directly to the LLM.
<script setup lang="ts">
const { addFiles, files, getPreviewUrl } = useThread()
function handleUpload(event: Event) {
const input = event.target as HTMLInputElement
if (input.files?.length) {
addFiles(input.files)
}
}
</script>
<template>
<input type="file" @change="handleUpload" multiple />
<div v-for="f in files" :key="f.id">
<img v-if="f.isImage" :src="getPreviewUrl(f)" :alt="f.name" />
<span>{{ f.name }}</span>
<span>{{ f.status }}</span> <!-- 'uploading' | 'ready' | 'committed' | 'error' -->
</div>
</template>Signature:
addFiles(files: File[] | FileList): voidThreadFile Type:
interface ThreadFile {
id: string
name: string
mimeType: string
size: number
isImage: boolean
localPreviewUrl: string | null
status: 'uploading' | 'ready' | 'committed' | 'error'
error?: string
path?: string
width?: number
height?: number
messageId?: string
}removeFile
Remove a pending file upload (cannot remove already committed files). A file that is still uploading has its HTTP request aborted, and it never reports ready or error afterwards.
<script setup lang="ts">
const { removeFile } = useThread()
</script>
<template>
<button @click="removeFile(file.id)">Cancel Upload</button>
</template>File URL Helpers
<script setup lang="ts">
const { getFileUrl, getThumbnailUrl, getPreviewUrl } = useThread()
// Full file URL (for download/viewing)
const url = getFileUrl(file)
// Thumbnail URL (for images)
const thumb = getThumbnailUrl(file)
// Preview URL (smart - uses local preview for pending, thumbnail for committed)
const preview = getPreviewUrl(file)
</script>stopExecution
Stop the current agent execution. Calling it when nothing is running is safe: the request succeeds and the thread is left unchanged.
<script setup lang="ts">
const { stopExecution } = useThread()
</script>
<template>
<button @click="stopExecution">Stop</button>
</template>deleteMessage
Delete a message from the thread. The message is optimistically removed from the UI immediately. If the server request fails, the message is restored.
<script setup lang="ts">
const { deleteMessage, messages } = useThread()
async function handleDelete(messageId: string) {
try {
await deleteMessage(messageId)
} catch (err) {
console.error('Failed to delete:', err)
}
}
</script>
<template>
<div v-for="msg in messages" :key="msg.id">
<span>{{ msg.content }}</span>
<button @click="handleDelete(msg.id)">Delete</button>
</div>
</template>Signature:
deleteMessage(messageId: string): Promise<void>Note: Server-side attachment files are automatically cleaned up when a message is deleted.
onEvent / subscribeToEvent
Subscribe to custom events emitted by the agent.
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
const { onEvent } = useThread()
let unsubscribe: (() => void) | null = null
onMounted(() => {
unsubscribe = onEvent<{ status: string }>('progress', (data) => {
console.log('Progress:', data.status)
})
})
onUnmounted(() => {
unsubscribe?.()
})
</script>addFiles vs addAttachment
| Feature | addFiles() |
addAttachment() |
|---|---|---|
| Purpose | Filesystem storage | Send to LLM |
| Upload timing | Immediate | On sendMessage() |
| Storage | Thread filesystem (persistent) | Thread filesystem (persistent), referenced by the message |
| Image processing | None (raw storage) | Server-side resize/optimize |
| Use case | Documents, persistent files | Chat images to LLM |
Message uploads carry base64 bytes in the request.
Real-time File Synchronization
The files computed ref from useThread() automatically stays synchronized with the server. When files are added, modified, or deleted on the thread (by the agent, tools, or other clients), the files array updates in real-time via WebSocket.
File Events
The SDK listens for three file-related events:
| Event | Description |
|---|---|
file_created |
A new file was added to the thread filesystem |
file_updated |
An existing file was modified |
file_deleted |
A file was removed from the thread filesystem |
Custom File Event Handling
You can also subscribe to these events directly for custom handling:
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useThread } from '@standardagents/vue'
const { onEvent } = useThread()
let unsubscribe: (() => void) | null = null
onMounted(() => {
unsubscribe = onEvent<{ path: string; file: { name: string; mimeType: string; size: number } }>('file_created', (data) => {
console.log('File created:', data.path)
// Custom notification, analytics, etc.
})
})
onUnmounted(() => unsubscribe?.())
</script>How It Works
- Initial load: When
ThreadProvidermounts, it fetches the current file list from the server - Live updates: WebSocket events update the
filesarray as changes occur - Optimistic UI: Local uploads via
addFiles()appear immediately as "uploading" then transition to "committed" - Deduplication: Server files take priority over local pending files with the same path
useThread Return Value
Full ThreadContext:
| Property | Type | Description |
|---|---|---|
threadId |
ComputedRef<string> |
The current thread ID |
messages |
ComputedRef<Message[]> |
Reactive array of raw messages |
hasMoreMessages |
ComputedRef<boolean> |
Whether older messages are available |
isLoadingOlderMessages |
ComputedRef<boolean> |
Whether an older message page is loading |
loadOlderMessages |
() => Promise<boolean> |
Load the next older message page |
loadMessagesByIds |
(ids: string[], options?) => Promise<boolean> |
Load specific messages. Set options.includeWorkblocks to load their complete workblocks |
workblocks |
ComputedRef<ThreadMessage[]> |
Messages transformed to workblocks (if useWorkblocks is true) |
subagentBlocks |
ComputedRef<SubagentThreadMessage[]> |
Messages transformed to subagent blocks (if useSubagentBlocks is true) |
groupedMessages |
ComputedRef<GroupedThreadMessage[]> |
The active grouped view: subagent blocks, then workblocks, then raw messages |
status |
ComputedRef<ConnectionStatus> |
Connection status |
connectionStatus |
ComputedRef<ConnectionStatus> |
Alias for status |
runStatus |
ComputedRef<ThreadStatus | null> |
Run status that the server sends: a snapshot on each connect, then each change. null until the first status arrives |
loading |
ComputedRef<boolean> |
Loading state |
isLoading |
ComputedRef<boolean> |
Alias for loading |
error |
ComputedRef<Error | null> |
Error state |
options |
ThreadProviderOptions |
Resolved options from ThreadProvider props |
sendMessage |
(payload) => Promise<SendMessageResponse> |
Send a message (auto-includes attachments) |
stopExecution |
() => Promise<void> |
Cancel execution |
deleteMessage |
(messageId: string) => Promise<void> |
Delete a message (optimistic UI) |
onEvent |
<T>(type, callback) => () => void |
Subscribe to events |
subscribeToEvent |
<T>(type, callback) => () => void |
Alias for onEvent |
files |
ComputedRef<ThreadFile[]> |
All files (pending uploads + committed) |
addFiles |
(files) => void |
Upload files to filesystem |
removeFile |
(id) => void |
Remove a pending file |
getFileUrl |
(file) => string |
Get file URL |
getThumbnailUrl |
(file) => string |
Get thumbnail URL |
getPreviewUrl |
(file) => string | null |
Get preview URL |
attachments |
Ref<PendingAttachment[]> |
Pending attachments for next message |
addAttachment |
(files) => void |
Add attachment(s) for next message |
removeAttachment |
(id) => void |
Remove a pending attachment |
clearAttachments |
() => void |
Clear all pending attachments |
ConnectionStatus:
type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected'ThreadStatus:
interface ThreadStatus {
state: 'idle' | 'running' | 'failed'
since: number // microseconds since epoch
reason?: string | null // stop reason of the last run; null while running
}status reports the WebSocket connection. runStatus reports the agent run. Read runStatus.value?.state === 'running' to find out whether a run is active.
Custom Events
Listen for custom events emitted by the backend using onEvent from useThread():
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { useThread } from '@standardagents/vue'
const { onEvent } = useThread()
const gameStatus = ref<{ success: boolean } | null>(null)
let unsubscribe: (() => void) | null = null
onMounted(() => {
unsubscribe = onEvent<{ success: boolean }>('game_built', (data) => {
gameStatus.value = data
})
})
onUnmounted(() => unsubscribe?.())
</script>
<template>
<div v-if="gameStatus">
Game ready: {{ gameStatus.success }}
</div>
</template>Backend Integration:
Tools and hooks emit custom thread events through their ThreadState parameter:
// Inside a tool or hook's execute callback:
state.emit("game_built", { success: true })Workblocks
Workblocks are a UI-friendly transformation of tool calls. When useWorkblocks: true, consecutive assistant messages with tool calls are grouped into workblocks.
Raw Messages
assistant (with tool_calls) → tool result → tool result → assistant (final)Transformed Workblocks
workblock {
workItems: [tool_call, tool_result, tool_call, tool_result]
status: 'completed' | 'pending' | 'failed'
} → assistant (final)WorkMessage Type
interface WorkMessage {
id: string
type: "workblock"
content: string | null
reasoning_content?: string | null
workItems: WorkItem[]
status: "pending" | "completed" | "failed"
created_at: number
depth?: number
}
interface WorkItem {
id: string
type: "tool_call" | "tool_result" | "content"
name?: string
content: string | null
status?: "pending" | "success" | "error" | null
tool_call_id?: string
}Rendering Workblocks
Use ThreadProvider with :useWorkblocks="true" and access via useThread():
<!-- Parent component sets up ThreadProvider with useWorkblocks -->
<template>
<ThreadProvider threadId="thread-id" :useWorkblocks="true">
<MessageList />
</ThreadProvider>
</template><!-- MessageList.vue -->
<script setup lang="ts">
import { useThread } from '@standardagents/vue'
const { workblocks } = useThread()
</script>
<template>
<div v-for="msg in workblocks" :key="msg.id">
<template v-if="'type' in msg && msg.type === 'workblock'">
<div :class="['workblock', msg.status]">
<div v-for="item in msg.workItems" :key="item.id">
<span v-if="item.type === 'tool_call'">Calling {{ item.name }}...</span>
<span v-else>{{ item.content }}</span>
</div>
</div>
</template>
<template v-else>
<div :class="msg.role">{{ msg.content }}</div>
</template>
</div>
</template>TypeScript Support
The package is written in TypeScript and includes full type definitions.
import type {
// Core types
Message,
WorkMessage,
WorkItem,
ThreadMessage,
Thread,
// Configuration
StandardAgentsConfig,
UseThreadOptions,
ThreadContext,
// Attachment types
PendingAttachment,
AttachmentPayload,
AttachmentRef,
ThreadFile,
// Connection and run status
ConnectionStatus,
ThreadStatus,
ThreadRunState,
} from "@standardagents/vue"Message Type
interface Message {
id: string
role: "system" | "user" | "assistant" | "tool"
content: string | null
name?: string | null
tool_calls?: string | null // JSON array
tool_call_id?: string | null
created_at: number // microseconds
status?: "pending" | "completed" | "failed"
silent?: boolean
tool_status?: "success" | "error" | null
reasoning_content?: string | null
parent_id?: string | null
depth?: number
attachments?: string | AttachmentRef[] | null
}HTTP message reads and message_data frames carry attachment arrays. The string form in the type is tolerance for older runtimes, whose frames could carry the stored JSON string. Use parseAttachments(message) when rendering SDK state.
Complete Example
Use the plugin setup above with your authenticated HTTP transport. Set support_agent to an agent name in your project.
View Complete Chat Interface Example with Image Attachments
<!-- App.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { ThreadProvider, useStandardAgents } from '@standardagents/vue'
import ChatInterface from './ChatInterface.vue'
const { client } = useStandardAgents()
const threadId = ref<string | null>(null)
async function startChat() {
const created = await client.createThread({ agent_id: 'support_agent' })
threadId.value = created.threadId
}
</script>
<template>
<button v-if="!threadId" @click="startChat">Start Chat</button>
<ThreadProvider v-else :threadId="threadId" :useWorkblocks="true">
<ChatInterface />
</ThreadProvider>
</template><!-- ChatInterface.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useThread } from '@standardagents/vue'
const input = ref('')
const {
workblocks,
status,
runStatus,
loading,
sendMessage,
stopExecution,
// Attachments for sending to LLM
attachments,
addAttachment,
removeAttachment,
} = useThread()
const isRunning = computed(() => runStatus.value?.state === 'running')
const handleSend = async () => {
if (!input.value.trim() || isRunning.value) return
await sendMessage({ role: 'user', content: input.value })
input.value = ''
}
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files?.length) {
addAttachment(target.files)
}
}
</script>
<template>
<div class="chat-container">
<p>Status: {{ status }} <span v-if="loading">(loading...)</span></p>
<!-- Message list -->
<div class="messages">
<div v-for="item in workblocks" :key="item.id">
<template v-if="'type' in item && item.type === 'workblock'">
<div class="workblock" :class="item.status">
<p v-if="item.content">{{ item.content }}</p>
<div v-for="work in item.workItems" :key="work.id" class="work-item">
<span v-if="work.type === 'tool_call'">Tool: {{ work.name }}</span>
<span v-if="work.type === 'tool_result'">{{ work.status }}</span>
</div>
</div>
</template>
<template v-else>
<div class="message" :class="item.role">
<strong>{{ item.role }}:</strong> {{ item.content }}
</div>
</template>
</div>
</div>
<!-- Pending attachments preview -->
<div v-if="attachments.length" class="attachment-preview">
<div v-for="a in attachments" :key="a.id" class="attachment-item">
<img v-if="a.isImage && a.previewUrl" :src="a.previewUrl" :alt="a.name" width="100" />
<span>{{ a.name }}</span>
<button @click="removeAttachment(a.id)">×</button>
</div>
</div>
<!-- Input area -->
<div class="input-area">
<input
type="file"
accept="image/*"
multiple
@change="handleFileSelect"
/>
<input
v-model="input"
@keyup.enter="handleSend"
:disabled="isRunning"
placeholder="Type a message..."
/>
<button @click="handleSend" :disabled="isRunning">Send</button>
<button v-if="isRunning" @click="stopExecution">Stop</button>
</div>
</div>
</template>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.messages {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.message.user {
align-self: flex-end;
background: #e3f2fd;
padding: 0.5rem 1rem;
border-radius: 1rem;
}
.message.assistant {
align-self: flex-start;
background: #f5f5f5;
padding: 0.5rem 1rem;
border-radius: 1rem;
}
.workblock {
background: #fff3e0;
padding: 1rem;
border-radius: 0.5rem;
}
.workblock.pending {
opacity: 0.7;
}
.attachment-preview {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.attachment-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: #f0f0f0;
border-radius: 0.25rem;
}
.input-area {
display: flex;
gap: 0.5rem;
}
.input-area input[type="text"] {
flex: 1;
padding: 0.5rem;
}
</style>Next Steps
Learn about agents, prompts, and tools
React hooks and components
How the backend framework works
REST and WebSocket APIs
Reconnect recovery
A thread provider reads the newest message page after a socket reconnect.
preload controls the initial history reads.
When preload is enabled, a snapshot after the first socket open closes the initial subscription gap.
Recovery also reads history when preload is false.
Recovery runs in the background.
Snapshot results preserve in-flight events and optimistic mutations.
Thread switches invalidate pending history responses.
Deletions reconcile within the newest page window.
Older cached rows outside that window remain until history is read again.
Shared HTTP transport
Pass a fetch-compatible function as fetch in StandardAgentsPlugin options.
Thread requests and endpoint overrides will inherit this transport.
app.use(StandardAgentsPlugin, { endpoint: '/api', fetch: requestFetch })An empty thread ID keeps a provider idle. Its children keep their component instances when the selection changes.
canReconnect checks an application auth halt before each retry. onAuthorized runs after an authorized socket opens. Both options are available on ThreadProvider and useThreadSetup.
Thread providers adapt a framework-free ThreadSessionStore from @standardagents/client. The store owns message history, live reducers, run status, committed files, and optimistic commands. Reconnect recovery runs once in that shared store. Upload queues and display grouping stay in the framework adapter. Socket frame types are available from @standardagents/client/protocol.
Store-owned thread state uses read-only computed refs. This includes messages, pagination flags, connection status, run status, loading and errors. Assignments to these refs fail typechecking. Thread commands update the shared session state. Pending attachment queues retain writable refs.