Skip to content

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.

ts
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

PropertyTypeNotes
connectionStatusRef<'disconnected' | 'connecting' | 'connected'>mirrors the client status event
isConnectedComputedRef<boolean>
isConnectingComputedRef<boolean>
isDisconnectedComputedRef<boolean>

Bar UI

PropertyTypeNotes
isBarOpenRef<boolean>toggled by the bar overlay
toggleBar()action
openBar()action
closeBar()action

Functions

PropertyTypeNotes
registeredFunctionsRef<AutopilotFunctionDescriptor[]>reactive copy of client.getRegisteredFunctions()
updateRegisteredFunctions()actionrefresh from the client (rarely needed — the store does this automatically on functionRegistered/functionUnregistered)

Context

PropertyTypeNotes
contextreactive(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)actionconvenience helper — set one key (same effect as context[key] = value)
updateContextProperties(updates)actionconvenience helper — shallow-merge keys
clearContext()actionconvenience helper — clear all keys
getContextSnapshot()actionplain-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.

ts
const store = useAutopilotStore();
store.context.selectedCity = 'Lisbon'; // auto-synced to the client via the watcher

setContextProperty('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

PropertyType
conversationRef<Conversation | null>
messagesRef<ConversationMessage[]>
hasConversationComputedRef<boolean>
messageCountComputedRef<number>
latestMessageComputedRef<ConversationMessage | null>
isLoadingMessagesRef<boolean>
lastMessageUpdateRef<number>

Actions: addMessage, updateMessage, updateMessageByRole, clearMessages, getMessagesByRole, getMessagesByCreator, refreshConversation.

Streaming

PropertyType
streamingMessageRef<ConversationMessage | null>
isStreamingRef<boolean>
hasStreamingMessageComputedRef<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 / actionTypeNotes
pendingConfirmationsRef<PendingConfirmation[]>calls awaiting a decision. Render an entry per item; resolving removes it from the list.
resolveConfirmation(id, decision, remember?)actiondecision is 'allow' | 'deny'. Pass remember: true with 'allow' to add the function name to the always-allow list.
alwaysAllowRef<Set<string>>function names auto-approved without prompting. Persisted in localStorage['autopilot:always-allow-functions'].
clearAlwaysAllow(name?)actiondrop one entry, or all when called with no argument.
clearPendingConfirmations()actionauto-deny everything in the queue. Called automatically on status === 'disconnected'.
ts
interface PendingConfirmation {
  id: string;
  name: string;
  arguments: unknown;
  timestamp?: number;
  resolve: (decision: 'allow' | 'deny') => void;
}
vue
<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 / actionTypeNotes
pendingAskUserRef<AskUserPrompt[]>prompts the server is waiting on. Empty when none. Cleared on status === 'disconnected'.
isSubmittingAskUserRef<boolean>true while a submitAskUser request is in flight.
submitAskUser(responses)actionposts 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

ts
visibleMessages: ComputedRef<ConversationMessage[]>;

The list a chat UI should actually render. It:

  1. Concatenates streamingMessage onto messages if streaming.
  2. Drops every message whose role is not user, assistant, or error.
  3. 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.

vue
<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

vue
<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>