Skip to main content

Overview

The @standardagents/vue package provides Vue 3 composables and a plugin for building UIs that connect to AgentBuilder threads. It handles real-time message streaming, custom event listening, and workblock transformation for tool visualization.

Key Features

  • 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/vue
pnpm
pnpm add @standardagents/vue
yarn
yarn add @standardagents/vue
Requirements: 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' })
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' })
Options:
OptionTypeRequiredDescription
endpointstringYesThe 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:
PropTypeDefaultDescription
threadIdstringRequiredThe thread ID to connect to
preloadbooleantrueFetch existing messages on mount
livebooleantrueEnable WebSocket for live updates
useWorkblocksbooleanfalseTransform tool calls into workblocks
depthnumber0Max message depth (0 = top-level only)
includeSilentbooleanfalseInclude silent messages
endpointstring-Override endpoint from plugin

Composables

useStandardAgents

Access the StandardAgents 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 thread = await client.createThread({ agent_id: 'my-agent' })
  console.log('Created:', thread.id)
}
</script>
Returns:
PropertyTypeDescription
clientAgentBuilderClientThe HTTP/WebSocket client instance
endpointstringThe 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,
  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. Automatically includes any pending attachments and clears them after sending.
<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>
Signature:
sendMessage(payload: Omit<SendMessagePayload, 'attachments'>): Promise<Message>

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): void
PendingAttachment 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): void

clearAttachments

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(): void

addFiles

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): void
ThreadFile 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).
<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.
<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

FeatureaddFiles()addAttachment()
PurposeFilesystem storageSend to LLM
Upload timingImmediateOn sendMessage()
StorageThread filesystem (persistent)Inline base64
Image processingNone (raw storage)Server-side resize/optimize
Use caseDocuments, persistent filesChat images to LLM

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:
EventDescription
file_createdA new file was added to the thread filesystem
file_updatedAn existing file was modified
file_deletedA 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

  1. Initial load: When ThreadProvider mounts, it fetches the current file list from the server
  2. Live updates: WebSocket events update the files array as changes occur
  3. Optimistic UI: Local uploads via addFiles() appear immediately as “uploading” then transition to “committed”
  4. Deduplication: Server files take priority over local pending files with the same path

useThread Return Value

Full ThreadContext:
PropertyTypeDescription
threadIdstringThe current thread ID
messagesRef<Message[]>Reactive array of raw messages
workblocksComputedRef<ThreadMessage[]>Messages transformed to workblocks (if useWorkblocks is true)
statusRef<ConnectionStatus>Connection status (alias: connectionStatus)
loadingRef<boolean>Loading state (alias: isLoading)
errorRef<Error | null>Error state
optionsThreadProviderOptionsOptions passed to the provider
sendMessage(payload) => Promise<Message>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) => () => voidSubscribe to events
filesComputedRef<ThreadFile[]>All files (pending uploads + committed)
addFiles(files) => voidUpload files to filesystem
removeFile(id) => voidRemove a pending file
getFileUrl(file) => stringGet file URL
getThumbnailUrl(file) => stringGet thumbnail URL
getPreviewUrl(file) => string | nullGet preview URL
attachmentsRef<PendingAttachment[]>Pending attachments for next message
addAttachment(files) => voidAdd attachment(s) for next message
removeAttachment(id) => voidRemove a pending attachment
clearAttachments() => voidClear all pending attachments
ConnectionStatus:
type ConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected'

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: Events are emitted from the backend using emitThreadEvent():
// In a tool or hook on the backend
import { emitThreadEvent } from "@standardagents/builder"

emitThreadEvent(flow, "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.
assistant (with tool_calls) → tool result → tool result → 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"
  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="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
  ConnectionStatus,
} 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 | null         // JSON array of AttachmentRef
}

Complete Example

<!-- App.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ThreadProvider } from '@standardagents/vue'
import ChatInterface from './ChatInterface.vue'

// Set auth token
onMounted(() => {
  localStorage.setItem('agentbuilder_auth_token', 'your-token')
})

const threadId = ref('123e4567-e89b-12d3-a456-426614174000')
</script>

<template>
  <ThreadProvider :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,
  loading,
  sendMessage,
  stopExecution,
  // Attachments for sending to LLM
  attachments,
  addAttachment,
  removeAttachment,
} = useThread()

const isRunning = computed(() => workblocks.value.some((m) => m.status === 'pending'))

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="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