2026 03 22 Vitto Native Agent Mode Design

Documentation for Plans

Vitto Native Agent Mode — IDE-Integrated AI Assistant

Session: S-future (handoff from S240) Date: 2026-03-22 Status: Spec Complete — Ready for Implementation Owner: apps/vitto-vscode-extension Predecessor: S240 (Unified Autonomy Engine), S184.5 (Panel Integration), S99 (Orchestration) Decision ID: dev_1774213181334_meo4zxt


Executive Summary

The Vitto VSCode extension currently embeds an Angular iframe for chat and a native Lit dashboard for MCP intelligence. The iframe is fundamentally sandboxed — it cannot read files, edit code, run commands, or interact with the IDE. This means Vitto can think but cannot act inside your editor.

This spec adds a Native Agent Mode — a Claude Code-level experience where Vitto reads your files, applies edits with diff preview, runs terminal commands, and tracks your active context. The AI reasoning happens on the Vitto backend (port 3003). The extension host executes tool calls locally via VSCode API. The user sees everything in a streaming chat within the existing MCP tab.

What makes this different from Claude Code: Vitto brings proactive intelligence (learned triggers), adaptive trust (earned autonomy), sub-agent orchestration (parallel specialists), and cross-session memory. Claude Code is a stateless CLI wrapper. Vitto is a persistent partner.


Table of Contents

  1. Architecture
  2. Extension Host: IDE Tool Service
  3. Extension Host: Agent Session Service
  4. Message Protocol Extensions
  5. Webview: Agent Chat View
  6. Context Providers
  7. Keybindings & Commands
  8. Security Model
  9. What Already Exists (Leverage Map)
  10. What Must Be Built (Gap List)
  11. Phasing
  12. Risks & Mitigations
  13. Non-Goals
  14. Key Files Reference

1. Architecture

User types in Agent Chat (Lit webview)
  │
  ▼
Webview sends { type: 'agentMessage', text: '...' }
  │
  ▼
┌─────────────────────────────────────────────────────┐
│           Extension Host (Node.js process)           │
│                                                      │
│  AgentSessionService                                 │
│    ├─ Opens SSE to Vitto backend (port 3003)        │
│    │   POST /engine/agent-session                    │
│    │   Body: { message, context, tools }             │
│    │   Response: SSE stream of events                │
│    │                                                  │
│    ├─ On SSE 'tool_use' event:                       │
│    │   ├─ IdeToolService.readFile(path)              │
│    │   ├─ IdeToolService.writeFile(path, content)    │
│    │   ├─ IdeToolService.runCommand(cmd)             │
│    │   ├─ IdeToolService.searchCode(pattern)         │
│    │   └─ IdeToolService.showDiff(path, newContent)  │
│    │                                                  │
│    ├─ Sends tool result back to SSE stream           │
│    │   POST /engine/agent-session/:id/tool-result    │
│    │                                                  │
│    └─ On SSE 'text' event:                           │
│        Forwards to webview via postMessage            │
│                                                      │
│  IdeToolService (VSCode API wrappers)                │
│    ├─ readFile()    → workspace.fs.readFile()        │
│    ├─ writeFile()   → WorkspaceEdit + applyEdit()   │
│    ├─ runCommand()  → Terminal + output capture      │
│    ├─ searchCode()  → workspace.findFiles() + grep   │
│    ├─ showDiff()    → commands.executeCommand(diff)  │
│    ├─ getContext()  → active editor + selection       │
│    └─ listFiles()   → workspace.findFiles(glob)      │
│                                                      │
│  ContextProviderService (passive context)             │
│    ├─ Active file path + language                     │
│    ├─ Selected text (if any)                          │
│    ├─ Open editor tabs                                │
│    ├─ Diagnostics (errors, warnings)                  │
│    ├─ Git status (modified files, branch)             │
│    └─ Workspace root + project type                   │
└─────────────────────────────────────────────────────┘
  │
  ▼
Webview renders streaming chat
  ├─ Text blocks (markdown)
  ├─ Tool use cards (file read, command run, diff)
  └─ Approval gates (write/delete require confirmation)

Key design decisions:

  1. Extension host does ALL IDE work. The webview only renders. This is how VSCode extensions must work — webviews are sandboxed.
  2. Vitto backend is the AI brain. The extension doesn't run LLMs. It sends the user message + IDE context to the backend, which runs the agent loop and streams tool calls back.
  3. SSE for streaming. Not WebSocket (already used for MCP events). SSE is simpler, works with existing fetch, and matches how the Angular app already communicates with Vitto.
  4. Tool calls are synchronous from the LLM's perspective. The backend sends a tool_use event, waits for the extension to execute it and POST the result back, then continues reasoning.

2. Extension Host: IDE Tool Service

This is the core — VSCode API wrappers that execute tool calls from the Vitto agent loop.

// NEW: apps/vitto-vscode-extension/src/services/ide-tool.service.ts

interface IdeToolResult {
  success: boolean;
  output: string;
  error?: string;
  metadata?: Record<string, unknown>;
}

class IdeToolService {

  /** Read file contents. Returns content + language ID. */
  async readFile(filePath: string): Promise<IdeToolResult> {
    const uri = this.resolveUri(filePath);
    const doc = await vscode.workspace.openTextDocument(uri);
    return {
      success: true,
      output: doc.getText(),
      metadata: { language: doc.languageId, lineCount: doc.lineCount },
    };
  }

  /**
   * Write file with diff preview.
   * Shows a diff editor so user can review before accepting.
   * If autoApprove=true (high trust), writes directly.
   */
  async writeFile(
    filePath: string,
    content: string,
    options?: { autoApprove?: boolean; createIfMissing?: boolean },
  ): Promise<IdeToolResult> {
    const uri = this.resolveUri(filePath);

    if (options?.autoApprove) {
      const encoder = new TextEncoder();
      await vscode.workspace.fs.writeFile(uri, encoder.encode(content));
      return { success: true, output: `Written: ${filePath}` };
    }

    // Show diff for user approval
    const tempUri = vscode.Uri.parse(`vitto-diff:${filePath}`);
    // Register content provider for the proposed changes
    this.diffContentProvider.setContent(tempUri, content);
    await vscode.commands.executeCommand('vscode.diff', uri, tempUri,
      `${path.basename(filePath)} (Vitto's changes)`);

    return {
      success: true,
      output: `Diff shown for ${filePath} — waiting for user review`,
      metadata: { awaitingApproval: true },
    };
  }

  /**
   * Run a shell command in a managed terminal.
   * Creates terminal if needed, captures output via shell integration.
   */
  async runCommand(
    command: string,
    options?: { cwd?: string; timeout?: number },
  ): Promise<IdeToolResult> {
    const cwd = options?.cwd || this.getWorkspaceRoot();
    const timeout = options?.timeout || 30_000;

    return new Promise((resolve) => {
      const terminal = vscode.window.createTerminal({
        name: `Vitto: ${command.slice(0, 30)}`,
        cwd,
        hideFromUser: false, // user should see what's running
      });

      terminal.show(true); // preserve focus on editor

      // Use shell integration API (VSCode 1.93+) to capture output
      const execution = terminal.shellIntegration?.executeCommand(command);
      if (execution) {
        const stream = execution.read();
        let output = '';
        const reader = stream.getReader();
        const readLoop = async () => {
          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            output += value;
          }
          resolve({ success: true, output });
        };
        readLoop();
      } else {
        // Fallback: send text, no output capture
        terminal.sendText(command);
        setTimeout(() => {
          resolve({
            success: true,
            output: `Command sent to terminal (output capture unavailable)`,
            metadata: { captureAvailable: false },
          });
        }, 1000);
      }

      // Timeout guard
      setTimeout(() => {
        resolve({
          success: false,
          output: '',
          error: `Command timed out after ${timeout}ms`,
        });
      }, timeout);
    });
  }

  /** Search code across workspace using ripgrep (via VSCode search API). */
  async searchCode(
    pattern: string,
    options?: { include?: string; maxResults?: number },
  ): Promise<IdeToolResult> {
    const results: string[] = [];
    const maxResults = options?.maxResults || 50;

    // Use findTextInFiles (available in Proposed API) or fallback to findFiles + readFile
    const files = await vscode.workspace.findFiles(
      options?.include || '**/*.{ts,js,html,scss,json}',
      '**/node_modules/**',
      100,
    );

    for (const file of files) {
      if (results.length >= maxResults) break;
      try {
        const doc = await vscode.workspace.openTextDocument(file);
        const text = doc.getText();
        const lines = text.split('\n');
        for (let i = 0; i < lines.length; i++) {
          if (lines[i].includes(pattern)) {
            results.push(`${vscode.workspace.asRelativePath(file)}:${i + 1}: ${lines[i].trim()}`);
          }
        }
      } catch { /* skip binary/large files */ }
    }

    return {
      success: true,
      output: results.join('\n') || 'No matches found',
      metadata: { matchCount: results.length },
    };
  }

  /** Show diff between current file and proposed changes. */
  async showDiff(filePath: string, proposedContent: string): Promise<IdeToolResult> {
    return this.writeFile(filePath, proposedContent, { autoApprove: false });
  }

  /** List files matching a glob pattern. */
  async listFiles(
    pattern: string,
    options?: { maxResults?: number },
  ): Promise<IdeToolResult> {
    const files = await vscode.workspace.findFiles(
      pattern,
      '**/node_modules/**',
      options?.maxResults || 200,
    );
    const paths = files.map(f => vscode.workspace.asRelativePath(f));
    return {
      success: true,
      output: paths.join('\n'),
      metadata: { fileCount: paths.length },
    };
  }

  /** Get diagnostics (errors, warnings) for a file or entire workspace. */
  async getDiagnostics(filePath?: string): Promise<IdeToolResult> {
    const diagnostics = filePath
      ? vscode.languages.getDiagnostics(this.resolveUri(filePath))
      : vscode.languages.getDiagnostics();

    const formatted = (Array.isArray(diagnostics[0]) ? diagnostics : [[null, diagnostics]])
      .flatMap(([uri, diags]: [vscode.Uri | null, vscode.Diagnostic[]]) =>
        diags.map(d => ({
          file: uri ? vscode.workspace.asRelativePath(uri) : filePath,
          line: d.range.start.line + 1,
          severity: vscode.DiagnosticSeverity[d.severity],
          message: d.message,
          source: d.source,
        }))
      );

    return {
      success: true,
      output: JSON.stringify(formatted, null, 2),
      metadata: { count: formatted.length },
    };
  }

  /** Resolve relative path to workspace URI. */
  private resolveUri(filePath: string): vscode.Uri {
    if (path.isAbsolute(filePath)) return vscode.Uri.file(filePath);
    const root = this.getWorkspaceRoot();
    return vscode.Uri.file(path.join(root, filePath));
  }

  private getWorkspaceRoot(): string {
    return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
  }
}

Critical design choices:


3. Extension Host: Agent Session Service

Manages the conversation lifecycle between extension and Vitto backend.

// NEW: apps/vitto-vscode-extension/src/services/agent-session.service.ts

interface AgentSessionOptions {
  message: string;
  context: IdeContext;
  sessionId?: string;        // resume existing conversation
  autonomyLevel?: string;    // from trust scoring
}

interface IdeContext {
  activeFile?: { path: string; language: string; content: string };
  selection?: { text: string; startLine: number; endLine: number };
  openFiles: string[];
  diagnostics: DiagnosticSummary[];
  gitStatus?: { branch: string; modified: string[]; staged: string[] };
  workspaceRoot: string;
  projectType: string;       // 'nx-monorepo', 'angular', 'node', etc.
}

class AgentSessionService {
  private currentSessionId: string | null = null;
  private abortController: AbortController | null = null;

  constructor(
    private config: ConfigService,
    private ideTools: IdeToolService,
    private contextProvider: ContextProviderService,
    private onEvent: (event: AgentStreamEvent) => void,
  ) {}

  /**
   * Start or continue an agent conversation.
   * Opens SSE stream to Vitto backend.
   * Handles tool_use events by executing locally and posting results back.
   */
  async chat(options: AgentSessionOptions): Promise<void> {
    this.abortController = new AbortController();

    const context = options.context || await this.contextProvider.gatherContext();
    const vittoUrl = this.config.getBackendUrl().replace(':3001', ':3003'); // vitto-api
    const token = this.config.getJwtToken();

    const sessionId = options.sessionId || this.currentSessionId || crypto.randomUUID();
    this.currentSessionId = sessionId;

    const response = await fetch(`${vittoUrl}/engine/agent-session`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`,
        'Accept': 'text/event-stream',
      },
      body: JSON.stringify({
        sessionId,
        message: options.message,
        context,
        source: 'vscode-extension',
        tools: this.getAvailableTools(),
        autonomyLevel: options.autonomyLevel,
      }),
      signal: this.abortController.signal,
    });

    if (!response.ok) {
      throw new Error(`Agent session failed: ${response.status} ${response.statusText}`);
    }

    // Parse SSE stream
    const reader = response.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop() || '';

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        const data = JSON.parse(line.slice(6));
        await this.handleStreamEvent(data, sessionId, vittoUrl, token);
      }
    }
  }

  /**
   * Handle a single SSE event from Vitto backend.
   */
  private async handleStreamEvent(
    event: VittoStreamEvent,
    sessionId: string,
    vittoUrl: string,
    token: string,
  ): Promise<void> {
    switch (event.type) {
      case 'text':
        // Forward AI text to webview for rendering
        this.onEvent({ type: 'text', content: event.content });
        break;

      case 'thinking':
        this.onEvent({ type: 'thinking', content: event.content });
        break;

      case 'tool_use': {
        // Execute tool locally via VSCode API
        this.onEvent({
          type: 'tool_start',
          tool: event.name,
          input: event.input,
        });

        const result = await this.executeTool(event.name, event.input);

        this.onEvent({
          type: 'tool_result',
          tool: event.name,
          result,
        });

        // POST result back to backend so agent loop continues
        await fetch(`${vittoUrl}/engine/agent-session/${sessionId}/tool-result`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${token}`,
          },
          body: JSON.stringify({
            toolUseId: event.id,
            result: result.output,
            error: result.error,
          }),
        });
        break;
      }

      case 'done':
        this.onEvent({ type: 'done', summary: event.summary });
        break;

      case 'error':
        this.onEvent({ type: 'error', message: event.message });
        break;
    }
  }

  /**
   * Execute a tool call using VSCode APIs.
   */
  private async executeTool(name: string, input: Record<string, unknown>): Promise<IdeToolResult> {
    switch (name) {
      case 'read_file':
        return this.ideTools.readFile(input.path as string);
      case 'write_file':
        return this.ideTools.writeFile(input.path as string, input.content as string, {
          autoApprove: input.autoApprove as boolean,
          createIfMissing: input.createIfMissing as boolean,
        });
      case 'run_command':
        return this.ideTools.runCommand(input.command as string, {
          cwd: input.cwd as string,
          timeout: input.timeout as number,
        });
      case 'search_code':
        return this.ideTools.searchCode(input.pattern as string, {
          include: input.include as string,
          maxResults: input.maxResults as number,
        });
      case 'list_files':
        return this.ideTools.listFiles(input.pattern as string);
      case 'get_diagnostics':
        return this.ideTools.getDiagnostics(input.path as string);
      case 'show_diff':
        return this.ideTools.showDiff(input.path as string, input.content as string);
      default:
        return { success: false, output: '', error: `Unknown tool: ${name}` };
    }
  }

  /** Tools the extension can execute locally. */
  private getAvailableTools(): string[] {
    return [
      'read_file', 'write_file', 'run_command', 'search_code',
      'list_files', 'get_diagnostics', 'show_diff',
    ];
  }

  /** Cancel current session. */
  cancel(): void {
    this.abortController?.abort();
    this.onEvent({ type: 'cancelled' });
  }
}

4. Message Protocol Extensions

New message types for the webview ↔ extension host bridge.

// MODIFIED: apps/vitto-vscode-extension/src/panels/webview/types.ts

// Add to MessageToExtension union:
| { type: 'agentMessage'; text: string; sessionId?: string }
| { type: 'agentCancel' }
| { type: 'agentApproveEdit'; path: string }
| { type: 'agentRejectEdit'; path: string }
| { type: 'getIdeContext' }

// Add to MessageFromExtension union:
| { type: 'agentText'; content: string }
| { type: 'agentThinking'; content: string }
| { type: 'agentToolStart'; tool: string; input: Record<string, unknown> }
| { type: 'agentToolResult'; tool: string; result: IdeToolResult }
| { type: 'agentDone'; summary: string }
| { type: 'agentError'; message: string }
| { type: 'agentCancelled' }
| { type: 'ideContext'; context: IdeContext }

5. Webview: Agent Chat View

New Lit component added as a tab in the MCP dashboard.

// NEW: apps/vitto-vscode-extension/src/panels/webview/components/agent/agent-chat-view.ts

@customElement('agent-chat-view')
class AgentChatView extends LitElement {
  @state() messages: AgentMessage[] = [];
  @state() isStreaming = false;
  @state() inputText = '';

  // Message types:
  // - user: user's input
  // - text: AI response (markdown)
  // - thinking: AI reasoning (collapsible)
  // - tool_start: tool about to execute (spinner)
  // - tool_result: tool output (collapsible code block)
  // - error: error message

  render() {
    return html`
      <div class="agent-container">
        <div class="agent-messages">
          ${this.messages.map(msg => this.renderMessage(msg))}
          ${this.isStreaming ? html`<loading-spinner size="small"></loading-spinner>` : ''}
        </div>

        <div class="agent-input-bar">
          <textarea
            .value=${this.inputText}
            @input=${(e) => this.inputText = e.target.value}
            @keydown=${this.handleKeydown}
            placeholder="Ask Vitto to read, edit, or run anything..."
            rows="1"
          ></textarea>
          <button
            class="send-btn"
            @click=${this.sendMessage}
            ?disabled=${this.isStreaming || !this.inputText.trim()}
          >
            ${this.isStreaming ? '■' : '▶'}
          </button>
        </div>

        <div class="agent-context-bar">
          <!-- Shows: active file, branch, error count -->
        </div>
      </div>
    `;
  }

  private renderMessage(msg: AgentMessage) {
    switch (msg.type) {
      case 'user':
        return html`<div class="msg msg-user">${msg.content}</div>`;
      case 'text':
        return html`<div class="msg msg-ai"><markdown-view .content=${msg.content}></markdown-view></div>`;
      case 'thinking':
        return html`
          <details class="msg msg-thinking">
            <summary>Thinking...</summary>
            <pre>${msg.content}</pre>
          </details>`;
      case 'tool_start':
        return html`
          <div class="msg msg-tool">
            <span class="tool-icon">⚡</span>
            <span class="tool-name">${msg.tool}</span>
            <span class="tool-input">${this.formatToolInput(msg)}</span>
            ${msg.pending ? html`<loading-spinner size="tiny"></loading-spinner>` : ''}
          </div>`;
      case 'tool_result':
        return html`
          <details class="msg msg-tool-result">
            <summary>${msg.tool} ${msg.result?.success ? '✓' : '✗'}</summary>
            <code-block .code=${msg.result?.output || msg.result?.error} language="text"></code-block>
          </details>`;
      case 'error':
        return html`<div class="msg msg-error">${msg.content}</div>`;
    }
  }

  private handleKeydown(e: KeyboardEvent) {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      this.sendMessage();
    }
  }

  private sendMessage() {
    if (this.isStreaming) {
      send({ type: 'agentCancel' });
      this.isStreaming = false;
      return;
    }

    const text = this.inputText.trim();
    if (!text) return;

    this.messages = [...this.messages, { type: 'user', content: text }];
    this.inputText = '';
    this.isStreaming = true;

    send({ type: 'agentMessage', text });
  }
}

Register in app.ts:

// Add to tabs array:
{ id: 'agent', label: 'Agent', icon: '⚡' }

// Add to view rendering:
case 'agent': return html`<agent-chat-view></agent-chat-view>`;

6. Context Providers

Automatically gathers IDE state to send with each agent message.

// NEW: apps/vitto-vscode-extension/src/services/context-provider.service.ts

class ContextProviderService {

  async gatherContext(): Promise<IdeContext> {
    const editor = vscode.window.activeTextEditor;
    const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';

    return {
      activeFile: editor ? {
        path: vscode.workspace.asRelativePath(editor.document.uri),
        language: editor.document.languageId,
        content: editor.document.getText(),
      } : undefined,

      selection: editor?.selection && !editor.selection.isEmpty ? {
        text: editor.document.getText(editor.selection),
        startLine: editor.selection.start.line + 1,
        endLine: editor.selection.end.line + 1,
      } : undefined,

      openFiles: vscode.window.tabGroups.all
        .flatMap(g => g.tabs)
        .filter(t => t.input instanceof vscode.TabInputText)
        .map(t => vscode.workspace.asRelativePath((t.input as vscode.TabInputText).uri)),

      diagnostics: this.getSummaryDiagnostics(),

      gitStatus: await this.getGitStatus(),

      workspaceRoot,
      projectType: this.detectProjectType(workspaceRoot),
    };
  }

  private getSummaryDiagnostics(): DiagnosticSummary[] {
    const all = vscode.languages.getDiagnostics();
    return all
      .filter(([, diags]) => diags.some(d => d.severity <= vscode.DiagnosticSeverity.Warning))
      .map(([uri, diags]) => ({
        file: vscode.workspace.asRelativePath(uri),
        errors: diags.filter(d => d.severity === vscode.DiagnosticSeverity.Error).length,
        warnings: diags.filter(d => d.severity === vscode.DiagnosticSeverity.Warning).length,
      }));
  }

  private async getGitStatus(): Promise<IdeContext['gitStatus'] | undefined> {
    const git = vscode.extensions.getExtension('vscode.git')?.exports?.getAPI(1);
    if (!git?.repositories?.length) return undefined;

    const repo = git.repositories[0];
    return {
      branch: repo.state.HEAD?.name || 'unknown',
      modified: repo.state.workingTreeChanges.map((c: any) =>
        vscode.workspace.asRelativePath(c.uri)),
      staged: repo.state.indexChanges.map((c: any) =>
        vscode.workspace.asRelativePath(c.uri)),
    };
  }

  private detectProjectType(root: string): string {
    const fs = require('fs');
    if (fs.existsSync(path.join(root, 'nx.json'))) return 'nx-monorepo';
    if (fs.existsSync(path.join(root, 'angular.json'))) return 'angular';
    if (fs.existsSync(path.join(root, 'next.config.js'))) return 'nextjs';
    if (fs.existsSync(path.join(root, 'package.json'))) return 'node';
    if (fs.existsSync(path.join(root, 'Cargo.toml'))) return 'rust';
    if (fs.existsSync(path.join(root, 'go.mod'))) return 'go';
    return 'unknown';
  }
}

7. Keybindings & Commands

// Add to package.json contributes.commands:
{
  "command": "vitto.agentChat",
  "title": "Vitto: Open Agent Chat"
},
{
  "command": "vitto.agentAskAboutSelection",
  "title": "Vitto: Ask About Selection"
},
{
  "command": "vitto.agentFixThis",
  "title": "Vitto: Fix This File"
}

// Add to package.json contributes.keybindings:
{
  "command": "vitto.agentChat",
  "key": "ctrl+shift+v",
  "mac": "cmd+shift+v"
},
{
  "command": "vitto.agentAskAboutSelection",
  "key": "ctrl+shift+a",
  "mac": "cmd+shift+a",
  "when": "editorHasSelection"
}

// Add to editor/context menu:
{
  "command": "vitto.agentAskAboutSelection",
  "group": "vitto@1",
  "when": "editorHasSelection"
},
{
  "command": "vitto.agentFixThis",
  "group": "vitto@2"
}

8. Security Model

Concern Policy
File writes Always show diff preview. Auto-approve only at AUTONOMOUS trust level for code_execution domain. User can always reject.
File reads Always allowed. Reading code is harmless. No approval needed.
Terminal commands Always visible in terminal panel. Never hidden. User sees the command before it runs.
Destructive operations rm, git reset, drop table — always require explicit approval regardless of trust level. Detected via pattern matching on command string.
Secret exposure IdeToolService strips .env file contents of values when returning to backend. Only key names are sent.
Workspace scope All file operations are restricted to workspace root. Path traversal (../../../etc/passwd) blocked by resolveUri().
Token security JWT stored in VSCode globalState (encrypted by VSCode). Never sent to webview. Only used in extension host fetch calls.
Rate limiting Max 100 tool calls per session. Prevents runaway loops. Enforced in AgentSessionService.

9. What Already Exists (Leverage Map)

Component Status Reuse
WebSocket to MCP gateway 780 lines, production Use for real-time events (handoffs, orchestration) alongside SSE for agent chat
Agent Executor Registry Plugin architecture, 3 executors Add VittoNativeExecutor as a new plugin
Message bridge (50+ types) Production Extend with 9 new agent message types
Lit dashboard (7 views) Production Add agent-chat-view as 8th tab
CodeLens provider Production Show "Ask Vitto" lens on functions
CodeActions provider Production Add "Fix with Vitto Agent" action
Config service Production Reuse for backend URL, auth token
Storage service Production Store agent session history
handleOpenFile() Already in main-panel.ts Reuse for navigation from tool results
Markdown renderer Lit component Reuse in agent chat messages
Code block renderer Lit component Reuse for tool outputs
Loading spinner Lit component Reuse for streaming states

Estimated reuse: 70% of existing code is directly leverageable.


10. What Must Be Built (Gap List)

Component Effort Priority
IdeToolService — VSCode API wrappers (7 tools) 1 session P0
AgentSessionService — SSE client + tool execution loop 1 session P0
ContextProviderService — IDE context gathering 0.5 session P0
agent-chat-view.ts — Lit chat component 1 session P0
Message protocol extensions (9 new types) 0.5 session P0
Main panel message handlers for agent types 0.5 session P0
Backend: /engine/agent-session SSE endpoint on vitto-api 1 session P0
Backend: /engine/agent-session/:id/tool-result POST 0.5 session P0
Keybindings + commands (3 new) 0.5 session P1
DiffContentProvider for write previews 0.5 session P1
Agent session history persistence 0.5 session P2
Total ~7 sessions

11. Phasing

Phase 1: Foundation (2 sessions)

Phase 2: Agent Loop (2 sessions)

Phase 3: Chat UI (1-2 sessions)

Phase 4: Commands & Polish (1-2 sessions)


12. Risks & Mitigations

Risk Severity Mitigation
Shell integration API not available (VSCode <1.93 or Cursor fork) High Fallback: terminal.sendText() without output capture. User sees output in terminal. Agent gets "command executed, check terminal for output"
SSE blocked by CORS (vitto-api on different port) Medium vitto-api already has CORS for localhost origins via cors.factory.ts. Add extension origin
Large file reads blowing context window Medium Cap file read at 100KB. For larger files, return first/last 500 lines with "[truncated]" marker
Git extension API not available Low Git context is optional. Graceful null return
User rejects every diff Low Track rejection rate. If >5 consecutive rejections, ask "Should I stop making changes?"
Backend endpoint doesn't exist yet High Must build /engine/agent-session on vitto-api. Reuses existing AgentLoopService — the endpoint is thin glue
Cursor may not support all VSCode APIs Medium Test each API. Maintain fallback map. Shell integration is the most likely gap

13. Non-Goals


14. Key Files Reference

Files to Create

apps/vitto-vscode-extension/src/
  ├── services/
  │   ├── ide-tool.service.ts              # VSCode API wrappers (7 tools)
  │   ├── agent-session.service.ts         # SSE client + tool execution loop
  │   ├── context-provider.service.ts      # IDE context gathering
  │   └── diff-content-provider.ts         # TextDocumentContentProvider for diffs
  └── panels/webview/
      └── components/agent/
          ├── agent-chat-view.ts           # Lit chat component
          └── agent-chat-view.css          # Styles (if not inline)

APIs/backend-vitto-api/src/app/engine/
  └── controllers/
      └── agent-session.controller.ts      # SSE endpoint for extension agent mode

Files to Modify

apps/vitto-vscode-extension/src/
  ├── extension.ts                         # Register new services + commands
  ├── panels/main-panel.ts                 # Add agent message handlers
  ├── panels/webview/types.ts              # Add 9 new message types
  ├── panels/webview/app.ts                # Add Agent tab
  ├── commands/index.ts                    # Register 3 new commands
  └── package.json                         # Add commands, keybindings, menu items

APIs/backend-vitto-api/src/app/engine/
  └── engine.module.ts                     # Register AgentSessionController

Handoff Checklist


End of spec. This builds on S240's Unified Autonomy Engine — trust scores gate auto-approval of file writes, proactive triggers can initiate agent sessions, and the memory system provides conversation continuity. The extension becomes Vitto's hands inside the IDE.