useAutopilotStore
A Pinia store that mirrors the headless client's state as reactive Vue refs. It is auto-initialized on first call: the store grabs useAutopilotClient() and subscribes to every relevant event. context is a directly-writable reactive object — mutate it (store.context.foo = 'bar') and a deep watcher auto-syncs the change to the client. Convenience actions remain available.
import { useAutopilotStore } from '@llmor/autopilot-vue';
const store = useAutopilotStore();Make sure the singleton is set first
The store calls useAutopilotClient() in its setup function. If you constructed your own client with new, call setAutopilotClient(client) before the first component reads from the store.
Reactive state
Connection
| Property | Type | Notes |
|---|---|---|
connectionStatus | Ref<'disconnected' | 'connecting' | 'connected'> | mirrors the client status event |
isConnected | ComputedRef<boolean> | |
isConnecting | ComputedRef<boolean> | |
isDisconnected | ComputedRef<boolean> |
Bar UI
| Property | Type | Notes |
|---|---|---|
isBarOpen | Ref<boolean> | toggled by the bar overlay |
toggleBar() | action | |
openBar() | action | |
closeBar() | action |
Functions
| Property | Type | Notes |
|---|---|---|
registeredFunctions | Ref<AutopilotFunctionDescriptor[]> | reactive copy of client.getRegisteredFunctions() |
updateRegisteredFunctions() | action | refresh from the client (rarely needed — the store does this automatically on functionRegistered/functionUnregistered) |
Context
| Property | Type | Notes |
|---|---|---|
context | reactive(Record<string, unknown>) | writable reactive object. Mutate it directly (context.foo = 'bar') and a deep watcher syncs the change to the client (100 ms debounce). Incoming server state is merged back in via echo-safe reconciliation. |
setContextProperty(key, value) | action | convenience helper — set one key (same effect as context[key] = value) |
updateContextProperties(updates) | action | convenience helper — shallow-merge keys |
clearContext() | action | convenience helper — clear all keys |
getContextSnapshot() | action | plain-object copy |
Context is bidirectional with echo safety. A deep watcher observes context and, on any mutation, sends a snapshot to the client (client.setContext); incoming server state flows back into context via the stateSynced / stateChanged events. The echo guard is layered: (1) a lastSnapshot check skips the push when the change originated from the server, and (2) even if that mis-fires, the client's diff is empty for a value it already holds, so nothing loops on the wire. Writes are eventually-consistent — synced on the next tick, not synchronously. Writes made while disconnected are buffered by the client and flushed on connect.
const store = useAutopilotStore();
store.context.selectedCity = 'Lisbon'; // auto-synced to the client via the watchersetContextProperty('selectedCity', 'Lisbon') still works as a convenience helper if you prefer a method call.
Activity log
recentActivity: Ref<Array<{ id, type, message, timestamp }>> — a rolling log of function calls and errors the bar's logs panel renders. Use addActivity(type, message) to push your own entries, clearActivity() to wipe.
activityLogCap: Ref<number> — how many entries the log keeps (newest first, default 20). Writable: store.activityLogCap = 100 — applied on the next entry.
The log also reflects context resolutions and the true wire order of function responses: Holding function responses: N pending context resolution(s) / Context resolutions cleared — function responses released entries track the holds, and Function response sent: <name> is logged from the functionResponseSent event — i.e. only after all holds have resolved and the state sync was acknowledged, which may be later than the Function <name> completed successfully (handler settlement) entry.
Function entries include the measured durations: Function navigate completed successfully (23ms) (handler time) and Function response sent: navigate (1.2s total) (request → wire).
Function call timing
functionCallHistory: Ref<FunctionCallRecord[]> — reactive mirror of client.getFunctionCallHistory(), refreshed on every function lifecycle event. visibleMessages uses it to graft duration_ms (handler) and total_ms (request → wire) onto each message's function_calls[], and passes the server's per-message meta (took, model token usage) through — the bar renders both: a duration badge on each function-call pill (with a handler/total breakdown in the expanded details) and took …s · N tokens next to the assistant message timestamp. formatDuration(ms) is exported for custom UIs.
Conversation
| Property | Type |
|---|---|
conversation | Ref<Conversation | null> |
messages | Ref<ConversationMessage[]> |
hasConversation | ComputedRef<boolean> |
messageCount | ComputedRef<number> |
latestMessage | ComputedRef<ConversationMessage | null> |
isLoadingMessages | Ref<boolean> |
lastMessageUpdate | Ref<number> |
Actions: addMessage, updateMessage, updateMessageByRole, clearMessages, getMessagesByRole, getMessagesByCreator, refreshConversation.
Streaming
| Property | Type |
|---|---|
streamingMessage | Ref<ConversationMessage | null> |
isStreaming | Ref<boolean> |
hasStreamingMessage | ComputedRef<boolean> |
Sensitive functions & confirmations
When a function registered with sensitive: true is invoked, the store's confirmation handler captures it and exposes it for your UI to resolve. See Sensitive functions for the underlying core API.
| Property / action | Type | Notes |
|---|---|---|
pendingConfirmations | Ref<PendingConfirmation[]> | calls awaiting a decision. Render an entry per item; resolving removes it from the list. |
resolveConfirmation(id, decision, remember?) | action | decision is 'allow' | 'deny'. Pass remember: true with 'allow' to add the function name to the always-allow list. |
alwaysAllow | Ref<Set<string>> | function names auto-approved without prompting. Persisted in localStorage['autopilot:always-allow-functions']. |
clearAlwaysAllow(name?) | action | drop one entry, or all when called with no argument. |
clearPendingConfirmations() | action | auto-deny everything in the queue. Called automatically on status === 'disconnected'. |
interface PendingConfirmation {
id: string;
name: string;
arguments: unknown;
timestamp?: number;
resolve: (decision: 'allow' | 'deny') => void;
}<script setup lang="ts">
import { useAutopilotStore } from '@llmor/autopilot-vue';
const store = useAutopilotStore();
</script>
<template>
<div v-for="p in store.pendingConfirmations" :key="p.id">
Autopilot wants to run <code>{{ p.name }}</code>
<pre>{{ JSON.stringify(p.arguments, null, 2) }}</pre>
<button @click="store.resolveConfirmation(p.id, 'deny')">Deny</button>
<button @click="store.resolveConfirmation(p.id, 'allow', true)">Always Allow</button>
<button @click="store.resolveConfirmation(p.id, 'allow')">Allow</button>
</div>
</template>The drop-in ConfirmationPrompt component does exactly this and is already wired into AutopilotOverlay.
Ask user
When the server suspends to ask the user structured questions, the store mirrors the client's pending prompts (see Ask user for the protocol).
| Property / action | Type | Notes |
|---|---|---|
pendingAskUser | Ref<AskUserPrompt[]> | prompts the server is waiting on. Empty when none. Cleared on status === 'disconnected'. |
isSubmittingAskUser | Ref<boolean> | true while a submitAskUser request is in flight. |
submitAskUser(responses) | action | posts AskUserResponseItem[] back via client.respondToAskUser. pendingAskUser updates from the resulting askUserRequired / askUserCleared events. |
The drop-in AskUserPrompt component renders the prompts as tabs and is already wired into AutopilotOverlay.
The visibleMessages computed
visibleMessages: ComputedRef<ConversationMessage[]>;The list a chat UI should actually render. It:
- Concatenates
streamingMessageontomessagesif streaming. - Drops every message whose role is not
user,assistant, orerror. - Merges consecutive empty assistant messages that only carry function calls — so multiple chained tool calls render as one bubble instead of several empty ones.
In practice you bind visibleMessages to your v-for, not messages.
<template>
<article v-for="msg in store.visibleMessages" :key="msg.created_at">
<strong>{{ msg.role }}</strong>: {{ msg.message }}
</article>
</template>A minimal custom chat component
<script setup lang="ts">
import { ref } from 'vue';
import { useAutopilotClient } from '@llmor/autopilot-core';
import { useAutopilotStore } from '@llmor/autopilot-vue';
const store = useAutopilotStore();
const client = useAutopilotClient();
const input = ref('');
async function send() {
if (!input.value.trim()) return;
await client.engageAutopilot(input.value);
input.value = '';
}
</script>
<template>
<section>
<div v-for="(msg, i) in store.visibleMessages" :key="i">
<strong>{{ msg.role }}</strong>: {{ msg.message }}
</div>
<form @submit.prevent="send">
<input v-model="input" :disabled="!store.isConnected" />
<button type="submit">Send</button>
</form>
</section>
</template>