~ vanish
~develop
$vanish ext new

Build for Vanish.
One object, the whole app.

Every extension is handed a single VanishAPI object, wired straight into the running application. Read state, register tools and views, drive the agent, paint the terminal — this page is the entire contract, nothing held back.

22 subsystemstypescript-firstpermissionedaudited at runtime
00

Getting started

onLoad(api)

An extension is a module that exports lifecycle hooks. Each hook receives the VanishAPI object. That is the only thing you need to know to write your first one.

Export a default object implementing the hooks you care about — onLoad, onUnload, onActivate, onDeactivate. Logic-only extensions can live in a functions/ folder instead; a default function there is treated as onLoad(api).

typescriptindex.ts — your extension entry point
import type { VanishAPI } from 'vanish-tui';

export default {
  async onLoad(api: VanishAPI) {
    api.log('My extension has been loaded!');
    // reach into any subsystem below
  },
  async onUnload(api: VanishAPI) {
    api.log('My extension is being unloaded!');
  },
};

A small manifest.json sits next to it. Declare the permissions you need up front — the user grants them under Settings › General › Security & Permissions.

jsonmanifest.json
{
  "name": "my_extension",
  "displayName": "My Extension",
  "version": "1.0.0",
  "description": "Does one small thing well.",
  "main": "index.ts",
  "permissions": ["agent"],
  "dependencies": []
}
Everything returns a cleanup

Registration methods (registerTool, registerSections, registerCommand, subscribe, on…) hand back an unregister function. Call them from onLoad, keep the disposers, and run them in onUnload so your extension unloads cleanly with no leaks.

The surface

22 subsystems, grouped six ways. Jump to any of them — or use the filter in the sidebar.

Foundation

The wiring every extension leans on — the event bus, the log buffer, and durable storage.

01

Events

api.events

Subscribe to and emit application-wide events.

The event system lets your extension react to changes across the whole app — the theme flips, extensions reload, a view comes into focus, a chat thread starts streaming. It is the spine that most other subsystems publish onto.

api.events.on(event, listener)
Register a listener for a named event.
api.events.off(event, listener)
Remove a previously registered listener.
api.events.emit(event, ...args)
Emit an event, calling every registered listener with the provided arguments.
api.events.once(event, listener)
Register a listener that fires at most once, then auto-removes.

Common core events

themeChangedname: stringThe active theme changed.
llmBusybusy: booleanThe active LLM started or stopped streaming.
extensionsReloadedExtensions reloaded from the active directory.
viewActivatedid: stringA view became the active view.
viewEnabledChanged{ viewId, enabled }A view was enabled or disabled.
chatSessionActivated{ sessionId }The active chat session changed.
chatThreadStarted{ threadId, sessionId, … }A chat request started streaming.
chatThreadChunk{ threadId, content, delta }Streamed content accumulated for a session.
chatThreadCompleted{ threadId, cancelled, message }A chat response finished.
mouse{ x, y, button, action, … }Terminal mouse activity, when tracking is on.
typescript
api.events.on('themeChanged', (themeName: string) => {
  api.log(`Theme was just changed to ${themeName}!`);
});
02

Logging

api.log

Read, subscribe, and write to the application log buffer.

api.log is both a function and a namespace. Call it to write a line (auto-prefixed with [Extension]); reach for its members to write at a specific level or to read the live buffer.

Writing

api.log(message)
Log a message at the info level.
api.log.info(message)
Log an informational message.
api.log.warn(message)
Log a warning.
api.log.error(message)
Log an error.
api.log.success(message)
Log a success message.
api.log.system(message)
Log a system-level message.

Reading & subscribing

api.log.getRecent(limit?)LogEntry[]
Most recent buffered entries in chronological order. Omit limit for the full retained buffer.
api.log.subscribe(listener)() => void
Fire a callback on every new entry. Returns an unsubscribe function.
typescriptLogEntry
interface LogEntry {
  id: number;
  level: 'log' | 'info' | 'warn' | 'error' | 'success' | 'system';
  message: string;
  timestamp: number; // Unix epoch milliseconds
}
typescript
const recentErrors = api.log
  .getRecent(100)
  .filter((entry) => entry.level === 'error');

const unsubscribe = api.log.subscribe((entry) => {
  if (entry.level === 'warn' || entry.level === 'error') {
    // react to errors or warnings
  }
});
03

Data & channels

api.data

Persist extension-owned files and coordinate live shared channels.

Two different needs, one namespace. Use file storage for artifacts that must survive a restart (indexes, snapshots, caches). Use shared channels for live cross-extension state that only needs to exist while Vanish is running — no globalThis, no temp files, no handshake events.

File storage

api.data.getRootDir()string
The Vanish root directory, typically ~/.vanish-tui.
api.data.saveData(path, value)
Save a string or JSON-serializable value to a namespaced file; returns the final path.
api.data.readData<T>(path)
Read a namespaced file. JSON is parsed; plain text returned as string; missing files return null.
api.data.editData<T>(path, editor)
Read, pass the current value to editor, save the return value, and return it.
api.data.deleteData(path)
Delete a namespaced file. Returns true when something was removed.

Shared channels

Channels are in-memory only. Each publisher namespace retains its latest value per channel, and subscribers receive the current snapshot immediately by default. Unloading an extension clears its retained publications.

api.data.publish(channel, value)
Publish or replace this extension's retained value for a channel.
api.data.unpublish(channel)
Remove this extension's retained value. Returns true if an entry existed.
api.data.readChannel<T>(channel)SharedChannelEntry<T>[]
Current retained snapshot as an array of publisher entries.
api.data.subscribe<T>(channel, listener, options?)() => void
Subscribe to snapshot updates. Immediately fires with the current snapshot unless emitCurrent: false.
typescriptMaintain an extension-owned index file
await api.data.editData('index.json', (current) => {
  const base = current && typeof current !== 'string'
    ? current
    : { version: 1, sessions: [] };

  return {
    ...base,
    sessions: [...base.sessions, { id: 'chat-123', title: 'New Session' }],
  };
});
typescriptPublish live data for other extensions to consume
api.data.publish('vanish:shared:hud-weather', {
  summary: '72F Clear',
  locationLabel: 'Brooklyn, NY',
  updatedAt: Date.now(),
});

const unsubscribe = api.data.subscribe('vanish:shared:hud-weather', (entries) => {
  const latest = entries.at(-1);
  api.log(`Current weather: ${latest?.value.summary ?? 'none'}`);
});

Rendering & motion

Draw to the terminal buffer, animate frame-by-frame, and surface overlay notifications.

04

Renderer

api.renderer

Paint cells and text directly onto the terminal buffer.

Low-level access to the terminal buffer for effects that aren't tied to a single view — transitions, floating progress bars, notification badges, persistent HUDs.

api.renderer.getSize(){ width, height }
Current terminal dimensions.
api.renderer.setCell(x, y, cell)
Set properties of one cell — char, fg, bg, bold, italic, dim.
api.renderer.drawText(x, y, text, options)
Draw a string starting at (x, y) with optional colors and styles.
api.renderer.registerOverlay(callback)() => void
Paint after the main UI renders. Returns a cleanup function.
api.renderer.reserveSpace(top, bottom)
Reserve fixed rows at the top/bottom; the core UI shrinks to avoid them. The way to build a HUD.
api.renderer.forceFullRender()
Bypass differential rendering and redraw every cell next frame.
typescriptA one-shot flash overlay on theme change
export default function (api: VanishAPI) {
  api.events.on('themeChanged', () => {
    let opacity = 1.0;
    const cleanup = api.animator.onFrame(() => {
      opacity -= 0.1;
      const { width, height } = api.renderer.getSize();
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
          api.renderer.setCell(x, y, { char: ' ', bg: '#ffffff' });
        }
      }
      if (opacity <= 0) {
        cleanup();
        api.renderer.forceFullRender();
      }
    });
  });
}
05

Animation

api.animator

Drive smooth, frame-by-frame transitions.

Vanish ships an animation engine that runs on every render frame. Pair api.animator with the easing library to make movement and color fades feel natural.

api.animator
The AnimationEngine instance — start, stop, and manage custom animations. Processes on every frame.
api.easing
Standard easing functions: linear, easeInQuad, easeOutCubic, easeInOutBounce, and more.
typescript
const progress = api.easing.easeOutQuart(elapsedTime / totalDuration);
06

Startup

api.startup

Customize the ASCII artwork shown on launch.

The startup animation shows ASCII art in the center and a progress bar at the bottom. Swap the art for your own — keep all lines a similar length for clean centering.

api.startup.getArtwork()string[]
The current ASCII artwork lines.
api.startup.setArtwork(artwork)
Set new ASCII artwork for the startup animation.
typescript
export default function (api: VanishAPI) {
  api.startup.setArtwork([
    ' __  __       _     __  __ ',
    '|  \/  |     | |   |  \/  |',
    '| \  / | ___ | |__ | \  / |',
    '| |\/| |/ _ \| \'_ \| |\/| |',
    '| |  | | (_) | | | | |  | |',
    '|_|  |_|\___/|_| |_|_|  |_|',
  ]);
}
07

Notifications

api.notifications

Overlay notifications with animations, themes, and actions.

Send overlay notifications that float above every view. They support custom positions, animations, themes, sizes, semantic tones, and action buttons.

api.notifications.send(options)NotificationHandle
Send a notification and get a handle to update or dismiss it.
api.notifications.dismiss(id)
Dismiss a notification by id. Returns true if found.
api.notifications.dismissAll()
Dismiss every active notification.
api.notifications.getActive()NotificationInfo[]
All active notification info objects.
typescriptNotificationOptions
interface NotificationOptions {
  title?: string;
  message: string;            // required
  duration?: number;          // ms; 0 = manual dismiss
  position?: NotificationPosition;
  animation?: string | NotificationAnimation;
  theme?: string | NotificationTheme;
  width?: number;
  height?: number;
  icon?: string;
  tone?: 'info' | 'success' | 'warning' | 'error';
  onDismiss?: () => void;
  dismissible?: boolean;      // default: true
  actions?: NotificationAction[];
}
typescriptPersistent notification, then update + dismiss
const handle = api.notifications.send({
  title: 'Download in Progress',
  message: 'Downloading update... 45%',
  tone: 'info',
  duration: 0, // never auto-dismiss
});

handle.update({ message: 'Downloading update... 78%' });
handle.dismiss();

Animations, themes & sizing

Use built-ins or register your own. Built-in animations: slide, fade, pop, bounce, drop, zoom, none. Built-in themes: default, minimal, accent, glass, sharp, borderless, toast.

api.notifications.animations.register(name, animation)
Register a custom enter/exit animation.
api.notifications.themes.register(name, theme)
Register a custom box style and color set.
api.notifications.size.setDefaults(config)
Adjust default min/max width and height (clamped to system maximums).
typescriptA notification with an action button
api.notifications.send({
  title: 'Search Extension',
  message: 'Agent permission is required for web search tools.',
  tone: 'warning',
  duration: 0,
  actions: [
    {
      id: 'permission-requests',
      label: 'Permission requests',
      shortcut: "Ctrl+'",
      onPress: () => api.events.emit('permissionRequests:open'),
    },
  ],
});

Chat & models

Orchestrate chat sessions and talk to the configured LLM providers.

08

Chat sessions

api.chat

Create, restore, fork sessions and substitute message rendering.

api.chat is session orchestration. Create new chats, restore saved conversations, switch the active session, and submit prompts without adding a view of your own. It also exposes one narrow rendering hook — registerMessageRenderer — that lets you substitute a message's body lines while the chat view keeps owning layout, scroll, and chrome.

api.chat.listSessions()ChatSessionSummary[]
Summaries, most recently updated first.
api.chat.getSession(id)
Full session data, or null.
api.chat.getActiveSessionId()
The active session id, or null.
api.chat.createSession(options?)ChatSessionSummary
Create a session; { title?, activate? }.
api.chat.activateSession(id)
Make an existing session active. Returns true on success.
api.chat.setSessionTitle(id, title)
Rename a session and emit the usual update events.
api.chat.sendMessage(content, options?){ accepted, sessionId, threadId }
Send a user message and stream the response in the background.
api.chat.deleteSession(id)
Remove a session; cancels its stream and switches away if it was active.
api.chat.forkSession(id, options)ForkChatSessionResult | null
Branch history at a chosen message index; the original is never modified.
api.chat.registerMessageRenderer(renderer)() => void
Per-message renderer the chat view consults when building visible lines.
api.chat.appendToComposer(text)
Insert text into the composer at the cursor and focus it. Never auto-sends.
api.chat.getAreaGeometry()ChatAreaGeometry | null
Absolute cell rects for the messages and input boxes — anchor overlays without guessing offsets.
typescriptChatSessionSummary
interface ChatSessionSummary {
  id: string;
  title: string;
  createdAt: string;
  updatedAt: string;
  messageCount: number;
  isStreaming: boolean;
  providerName: string | null;
  providerDisplayName: string | null;
  model: string | null;
  preview: string;
}
typescriptStart a background thread, then bring the UI back to chat
const session = api.chat.createSession({ title: 'Refactor Notes', activate: true });
const result = api.chat.sendMessage('Summarize the tradeoffs of this refactor.', {
  sessionId: session.id,
});

if (result.accepted) {
  await api.views.activate('chat');
}

Message renderers

typescript
const off = api.chat.registerMessageRenderer({
  id: 'demo-bullets',
  priority: 10,
  filter: (m) => m.role === 'assistant',
  render: (m, ctx) => {
    const lines = ctx.defaultRender();
    if (lines.length === 0) return null;
    lines[0] = { ...lines[0], text: '• ' + lines[0].text };
    return lines;
  },
});
09

LLM providers

api.llmllm

Query providers, read model presets, and run completions.

Vanish integrates multiple providers (Groq, Google AI Studio, and more). Read the active provider for low-level generation, or — better — use the preset completion APIs that resolve the right provider and model from the user's four configured presets (heavy, medium, chat, lite).

Provider access

api.llm.getProvider()LLMProvider
The active provider — exposes complete(), stream(), isConfigured().
api.llm.setProvider(name)
Set the active provider. Returns true on success.
api.llm.listProviders()
Available provider metadata.
api.llm.isBusy()
True while the LLM is generating.

Model presets

api.llm.getPresetModel(preset)
Model name for 'heavy' | 'medium' | 'chat' | 'lite', or null.
api.llm.setPresetModel(preset, model)
Set the model for a preset. Returns true, or false if denied.
api.llm.getPresets()
All preset configurations at once.

Direct completion APIs

The convenient path. api.llm.response.{heavy,medium,chat,lite}(messages, options?) returns a Promise<CompletionResult>; api.llm.stream.{…} returns an AsyncGenerator<StreamChunk>. If a preset or provider isn't configured, a notification is shown to the user and an error is thrown.

typescriptMessage · CompletionOptions · CompletionResult
interface Message {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

interface CompletionOptions {
  temperature?: number;
  maxTokens?: number;
  topP?: number;
  stop?: string[];
}

interface CompletionResult {
  content: string;
  finishReason?: 'stop' | 'length' | 'content_filter';
  usage?: { promptTokens: number; completionTokens: number; totalTokens: number };
}
typescriptHeavy model for complex reasoning
try {
  const result = await api.llm.response.heavy([
    { role: 'system', content: 'You are a code reviewer.' },
    { role: 'user', content: 'Review: function add(a,b){return a+b}' },
  ], { temperature: 0.3, maxTokens: 1000 });
  api.log(`Heavy response: ${result.content}`);
} catch (error) {
  // notification already shown to the user
  api.log.error(`Heavy model not available: ${error.message}`);
}
typescriptChat model with streaming
let full = '';
for await (const chunk of api.llm.stream.chat([
  { role: 'user', content: 'Tell me a short joke' },
])) {
  if (chunk.content) full += chunk.content;
  if (chunk.done) api.log(`Stream complete: ${full}`);
}

The agent

Register tools, shape the system prompt, and evolve prompt skills over time.

10

Agent harness

api.agentagent

A turn-by-turn tool-calling engine between user messages and the LLM.

When the harness is active it intercepts each outbound message, runs a structured loop — call tools, feed results back to the LLM — and delivers only the final assistant response to the chat. It is transparent when idle: with no tools registered and an unmodified prompt, isActive() is false and chat streams exactly as before, with zero overhead.

The loop

  1. The user sends a message in the chat view.
  2. The harness builds the prompt: composed system prompt + tool definitions + history.
  3. It calls the LLM. If the response contains <tool_call> blocks, each tool runs and results are appended as <tool_result> blocks.
  4. The updated conversation goes back to the LLM and the loop repeats.
  5. Once the LLM responds with no tool calls, that text becomes the final assistant message.
  6. If maxTurns is hit first, the harness reports an error and stops.

Tools

api.agent.registerTool(tool)() => void
Register a tool; returns an unregister function. Throws on duplicate name.
api.agent.unregisterTool(name)
Remove a tool by name. Returns true if found.
api.agent.listTools()
Summary of registered tools, including the six built-in defaults.
api.agent.hasTool(name)
True if a tool with that name is registered.
typescriptToolDefinition
interface ToolDefinition {
  name: string;          // globally unique, kebab-case
  description: string;   // shown to the LLM
  parameters: {
    type: 'object';
    properties: Record<string, ToolParameter>;
    required?: string[];
  };
  annotations?: ToolAnnotations; // readOnlyHint, destructiveHint, …
  timeoutMs?: number;
  handler: (
    args: Record<string, unknown>,
    context?: ToolHandlerContext, // { signal, onProgress }
  ) => ToolResult | Promise<ToolResult>;
}
typescriptA read-only tool that fetches a URL
const unregister = api.agent.registerTool({
  name: 'fetch-url',
  description: 'Fetches the plain-text content of a URL.',
  parameters: {
    type: 'object',
    properties: {
      url: { type: 'string', description: 'Full URL including https://' },
    },
    required: ['url'],
  },
  annotations: { readOnlyHint: true, idempotentHint: true },
  handler: async (args) => {
    const res = await fetch(args.url as string);
    if (!res.ok) return { content: `HTTP ${res.status}`, isError: true };
    return { content: (await res.text()).slice(0, 4000) };
  },
});

Built-in tools

Six read-only tools are always present: get-current-time, get-system-info, get-working-directory, get-theme-info, list-views, list-providers.

System prompt & profiles

api.agent.getSystemPrompt()
The fully composed prompt (base + all segments).
api.agent.appendSystemPrompt(segment, id?)
Add a segment after the base prompt; id lets you replace it later.
api.agent.prependSystemPrompt(segment, id?)
Add a segment before the base prompt.
api.agent.removeSystemPromptSegment(id)
Remove a segment by id.
api.agent.setPromptProfile(profile)
Add structured guardrails / toolUseGuidance / finalAnswerGuidance without clobbering the base prompt.
api.agent.setResponseContract(contract)
Enforce requiredPhrases / forbiddenPhrases on final answers.
typescriptAdd guardrails instead of replacing the base prompt
api.agent.setPromptProfile({
  guardrails: [
    'Never claim a file was changed unless a tool result in this run confirms it.',
    'If a tool fails, explain the blocker plainly instead of pretending it succeeded.',
  ],
  finalAnswerGuidance: ['State what you verified before giving the conclusion.'],
});

Config, state & events

api.agent.setConfig(config)
Merge into HarnessConfig — maxTurns (default 10), toolTimeout (default 30000), requireConfirmationForDestructiveTools (default true).
api.agent.isActive()
True if the harness will intercept the next message.
api.agent.onToolCall(listener)() => void
Fires before a tool runs.
api.agent.onToolResult(listener)() => void
Fires after a tool returns.
agentToolCall{ threadId, call }Immediately before a tool handler runs.
agentToolProgress{ call, chunk }Each incremental progress chunk.
agentToolResult{ call, result }Immediately after a handler returns.
agentTurnComplete{ turnNumber }After all tools in a turn finish.
agentRunComplete{ totalTurns }When the harness returns the final response.
11

Evolve

api.evolveagent

Register evolvable prompt skills and observe promotions.

Evolve treats some prompt segments as skills: frozen metadata plus a mutable markdown body. Extensions register skills they own; only the body changes through evolve runs — never the frontmatter or segment id. The agent permission gates every operation.

api.evolve.registerSkill(registration)() => void
Register an evolvable skill; returns a disposer that removes the segment.
api.evolve.getSkillStatus(id)
Runtime metadata including activeVersion and lastSeenBodyHash, or null.
api.evolve.onSkillUpdated(listener)() => void
Fires after a promotion or rollback with { id, version, bodyHash }.
api.evolve.getStatus()
{ enabled, capture, noTraceSession, schemaTooNew, warnings } — surface evolve state in your UI.
typescriptSkillRegistration
interface SkillRegistration {
  id: string;            // kebab-case, globally unique
  name: string;
  description: string;   // doubles as the judge's rubric anchor
  segmentId: string;     // harness segment this body backs
  position: 'prepend' | 'append';
  defaultBody: string;   // persisted as version 1
}
typescriptRegister a final-answer-style skill
const dispose = await api.evolve.registerSkill({
  id: 'final-answer-style',
  name: 'Final answer style',
  description: 'Keep final answers concise and action-oriented.',
  segmentId: 'vanish-agent-final-answer-style',
  position: 'append',
  defaultBody: [
    'Style guidance for final answers:',
    '- Lead with the answer in the first sentence.',
    '- Avoid restating the question.',
  ].join('\n'),
});
evolve:skillRegistered{ id }After a skill is registered.
evolve:skillUpdated{ id, version, bodyHash }After a successful promotion or rollback.
evolve:runFinished{ runId, skillId, result }After an evolve run completes.

Interface surfaces

Themes, modals, settings, the command palette, views, and keyboard input.

12

Themes

api.getTheme

Read and change the active theme.

Read the active theme or any registered theme, and switch between them. Custom themes your extension registers are automatically available here.

api.getTheme()Theme
The currently active theme object (all colors and text styles).
api.getThemeByName(name)Theme | null
A theme by name, or null if it doesn't exist.
api.setTheme(name)
Set the active theme. Returns true on success and emits themeChanged.
api.listThemes()string[]
Names of all registered themes.
api.getThemes()Theme[]
Every registered theme object.
typescript
const current = api.getTheme();
api.log(`Current background: ${current.colors.background}`);

if (api.listThemes().includes('my-custom-theme')) {
  api.setTheme('my-custom-theme');
}
13

Modals

api.modal

Open and close stacking overlay modals.

Present overlay modals that block the main views until dismissed — alerts, confirmations, input prompts. Modals stack; opening a new one displays it on top.

api.modal.open<T>(modal, onClose?)
Open a modal extending the core BaseModal. onClose receives the result.
api.modal.closeTop()
Close the top-most modal.
api.modal.closeAll()
Close every open modal.
api.modal.hasModals()
True if at least one modal is open.
typescript
import { BaseModal } from 'vanish-tui';

class AlertModal extends BaseModal<boolean> {
  // ... modal implementation ...
}

const alert = new AlertModal('Warning', 'Are you sure?');
api.modal.open(alert, (result) => {
  api.log(result ? 'User confirmed.' : 'User cancelled.');
});
14

Settings

api.settings

Register custom settings sections and control element visibility.

Inject your own settings sections into the built-in Settings view, or toggle the visibility of existing elements. Register a section with the same id as an existing one and the core merges your elements into it.

api.settings.registerSections(sections)() => void
Register custom sections; returns a cleanup function.
api.settings.getSections()
All registered SettingsSection objects.
api.settings.setElementVisibility(id, visible)
Show or hide a settings element.
api.settings.registerPresentation(presentation)() => void
Change how the settings view is grouped or displayed.
typescriptA boolean setting backed by structured persistence
const cleanup = api.settings.registerSections([{
  id: 'my-extension',
  title: 'My Extension Config',
  elements: [{
    id: 'enable-feature-x',
    type: 'boolean',
    label: 'Enable Feature X',
    value: 'Enabled',
    editable: true,
    sectionId: 'my-extension',
    persistence: {
      namespace: 'myExtension',
      key: 'enabled',
      fromStored: (s) => (s === true ? 'Enabled' : 'Disabled'),
      toStored: (v) => v === 'Enabled',
    },
    onActivate: async () => ({ message: 'Feature X toggled.', tone: 'success' }),
  }],
}]);
15

Command palette

api.commands

Register first-class palette commands and drive the palette surface.

The palette (default Ctrl+Shift+P) surfaces every invocable action. Settings, theme switches, and view navigation are auto-adopted, so it's populated on day one. Register your own commands to join the fuzzy-scored results, MRU history, pinning, and parameterized sub-pick flows.

api.commands.registerCommand(command)() => void
Register one command; returns a cleanup function.
api.commands.registerCommands(commands)() => void
Batch-register with atomic rollback if any definition is invalid.
api.commands.registerCategory(category)() => void
Add a custom category with label, glyph, and sort priority.
api.commands.open(options?)
Open the palette; resolves with the CommandResult that ran, or null.
api.commands.run(id, args?)
Run a command by id without opening the palette.
api.commands.list(options?)CommandSummary[]
All commands, optionally filtered to a category.
typescriptCommandContext (handler argument)
interface CommandContext {
  sourceViewId: string | null;
  query: string;
  mode: '>' | '#' | '@' | '?' | ':' | '/' | null;
  pickFrom: <T>(items: PickableItem<T>[], prompt: string) => Promise<T | null>;
  promptFor: (options: PromptOptions) => Promise<string | null>;
  confirm: (options: { message: string; confirmWord?: string }) => Promise<boolean>;
  runCommand: (id: string, args?: Record<string, unknown>) => Promise<CommandResult>;
  namespace: string;
}
typescriptA command with a parameterized sub-pick
api.commands.registerCommand({
  id: 'my-ext.pick-rule',
  label: 'Disable rule…',
  category: 'extensions',
  handler: async (ctx) => {
    const rule = await ctx.pickFrom(
      [
        { id: 'no-console', label: 'no-console', value: 'no-console' },
        { id: 'no-debugger', label: 'no-debugger', value: 'no-debugger' },
      ],
      'Disable which rule?',
    );
    if (!rule) return { message: 'Cancelled.', tone: 'info' };
    return { message: `Disabled ${rule}.`, tone: 'success' };
  },
});
16

Views

api.views

Inspect, enable/disable, and reorder views.

Programmatically inspect, switch, enable/disable, and reorder views — for view managers, shortcut toggles, or dynamic UI based on user preference. Disabling the active view auto-switches to the first enabled one.

api.views.listRegistered(){ id, name }[]
Registered view metadata.
api.views.getCurrent()
Id of the active view, or null.
api.views.activate(viewId)
Switch to a view. Resolves true on success.
api.views.isEnabled(viewId)
True if the view is enabled and reachable.
api.views.setEnabled(viewId, enabled)
Enable/disable a view; emits viewEnabledChanged.
api.views.getOrder()string[]
View ids in display order.
api.views.setOrder(order)
Set sidebar order; emits viewOrderChanged.
typescript
// toggle a view
const chatEnabled = api.views.isEnabled('chat');
api.views.setEnabled('chat', !chatEnabled);

// reverse the sidebar order
api.views.setOrder([...api.views.getOrder()].reverse());

api.events.on('viewEnabledChanged', ({ viewId, enabled }) => {
  api.log(`View ${viewId} was ${enabled ? 'enabled' : 'disabled'}`);
});
17

Input

api.input

Intercept keys and drive view-aware navigation.

Intercept keyboard activity before it reaches the current view, and invoke the same navigation actions the core UI uses. Mouse activity comes through api.events, not here.

api.input.getContext(){ currentViewId, mode, modalOpen }
The current view and whether a modal is open.
api.input.registerHandler(handler)
Register a key interceptor; return true from it when you handle a key.
api.input.navigate(command)
Invoke 'up' | 'down' | 'left' | 'right' | 'activate'.
api.input.nextView() / prevView()
Switch to the next / previous enabled view.
typescriptKeyPress
interface KeyPress {
  name: string;
  ctrl: boolean;
  meta: boolean;
  alt: boolean;
  shift: boolean;
  sequence: string; // raw terminal bytes
  text?: string;    // printable character, when reported
}
typescript
api.input.registerHandler((key, context) => {
  if (context.currentViewId !== 'chat' || context.modalOpen) return false;
  if (key.ctrl && key.name === 'h') {
    api.log('Open chat history modal');
    return true;
  }
  return false;
});

api.events.on('mouse:scroll', (e) => {
  if (e.scrollDelta < 0) api.log('Scrolled up');
});

Platform & security

Location, encrypted secrets, permissions, the outbound-signal recorder, and extension management.

18

Location

api.locationlocation

Access secure, IP-derived user location.

Access the user's location, detected via IP-based geolocation and cached by the core. Restricted — requires explicit authorization under Security & Permissions.

api.location.get()Promise<{ lat, lon, city, region } | null>
The current location, or null if denied/unavailable.
api.location.getSummary()string | null
A pre-formatted summary like "New York, New York" — ideal for HUDs.
typescript
const location = await api.location.get();
if (location) {
  api.log(`Detected: ${location.city}, ${location.region}`);
} else {
  api.log('Location access denied or unavailable.');
}

const summary = api.location.getSummary();
if (summary) api.renderer.drawText(0, 0, summary);
19

Secrets

api.secretssecrets

Encrypted, namespaced key-value storage.

A secure, encrypted local store for API keys and tokens. Each extension gets a namespace-scoped helper for its own values; cross-extension access is an explicit opt-in via forNamespace.

api.secrets.namespace
The unique namespace string for this extension.
api.secrets.get(key)Promise<string | null>
Retrieve and decrypt a secret.
api.secrets.set(key, value)
Encrypt and store a value.
api.secrets.delete(key)
Delete a secret. Resolves true if one existed.
api.secrets.has(key)
Check existence without decrypting.
api.secrets.forNamespace(namespace)
A helper for another extension's namespace — same API.
typescript
const tokenKey = 'github-personal-access-token';
if (!(await api.secrets.has(tokenKey))) {
  await api.secrets.set(tokenKey, 'ghp_example123');
  api.log('Token securely stored!');
} else {
  const token = await api.secrets.get(tokenKey);
  api.log('Retrieved stored token securely.');
}
bash
vanish audit secrets                    # last 100 entries, all extensions
vanish audit secrets -x my_extension    # filter to one extension
vanish audit secrets --json -n 500      # JSON output, last 500 entries
20

Permissions

api.permissions

Introspect, request, and (when privileged) resolve permissions.

Each extension tracks four flags: secrets, llm, location, agent. Introspect what you have, request what you don't, and — with the permissionManager capability — resolve other extensions' pending requests.

Reading & requesting

api.permissions.getAll(){ secrets, llm, location, agent }
Current flags for this extension.
api.permissions.hasPermission(permission)
True if the named permission is granted.
api.permissions.check(required){ granted, denied }
A synchronous gate before permission-sensitive work.
api.permissions.request(required, options?)PermissionRequestResult
Record a pending request and surface an actionable notification.

Managing requests

Read methods are open to everyone; mutating methods are gated on the manifest capability permissionManager (one owner per capability).

api.permissions.requests.list()PermissionRequest[]
Current pending requests.
api.permissions.requests.onChanged(listener)() => void
Subscribe to the request registry.
api.permissions.requests.resolve(ext, perm, granted)
Capability-gated: resolve one request.
api.permissions.requests.resolveAll(granted)
Capability-gated: bulk-resolve every pending request.
typescript
const { denied } = api.permissions.check(['agent']);
if (denied.includes('agent')) {
  api.permissions.request(['agent'], {
    title: 'Search Extension: Agent permission required',
    message: 'Needs agent access to expose web search tools.',
    duration: 0,
  });
}
jsonDeclare the capability to resolve requests
{
  "name": "notify_request_perms",
  "capabilities": ["permissionManager"]
}
21

Outguard

api.outguard

Inspect and report outbound extension signals.

Outguard is the outbound-signal recorder. Vanish automatically records extension-attributed fetch(...) calls plus calls through protected surfaces (LLM presets, location). It redacts common secret query params (token, api_key, secret, password, …) before writing a signal.

api.outguard.report(signal)
Manually record a custom outbound signal for non-fetch integrations.
api.outguard.getRecent(limit?)
Recent signals for this extension (default 50).
api.outguard.getRecentNotifications(limit?)
Recent extension-sent notification records (default 50).
typescript
api.outguard.report({
  kind: 'custom',
  target: 'vendor-sdk:lookup',
  metadata: { lookupType: 'symbol', symbol: 'MSFT' },
});

const recent = await api.outguard.getRecent(10);
api.log(`Outguard has ${recent.length} recent signal(s).`);
22

Extension management

api.extensions

Inspect, enable/disable, and reload installed extensions.

Inspect the extensions discovered in the active directory and toggle them — useful for managers, compatibility checkers, and installer flows.

api.extensions.listInstalled()Promise<InstalledExtension[]>
Metadata for every discovered extension.
api.extensions.getInstalled(name)
One extension's metadata, or null.
api.extensions.isEnabled(name)
Whether the named extension is currently enabled.
api.extensions.setEnabled(name, enabled)
Enable/disable and reload the extension system. Resolves true if state changed.
api.extensions.reload()
Force a full extension reload.
jsonDeclaring dependencies in manifest.json
{
  "name": "vanish_hud_plus",
  "displayName": "Vanish HUD Plus",
  "version": "1.0.0",
  "dependencies": ["vanish_hud"],
  "main": "main.ts"
}
typescriptWarn before disabling an extension others depend on
async function safeDisable(api: VanishAPI, name: string) {
  const ext = await api.extensions.getInstalled(name);
  if (!ext) return;
  if (ext.dependents.length > 0) {
    api.notifications.send({
      title: 'Dependents Warning',
      message: `Disabling "${ext.displayName}" affects: ${ext.dependents.join(', ')}`,
      tone: 'warning',
      duration: 5000,
    });
  }
  await api.extensions.setEnabled(name, false);
}