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
- Architecture
- Extension Host: IDE Tool Service
- Extension Host: Agent Session Service
- Message Protocol Extensions
- Webview: Agent Chat View
- Context Providers
- Keybindings & Commands
- Security Model
- What Already Exists (Leverage Map)
- What Must Be Built (Gap List)
- Phasing
- Risks & Mitigations
- Non-Goals
- 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:
- Extension host does ALL IDE work. The webview only renders. This is how VSCode extensions must work — webviews are sandboxed.
- 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.
- 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.
- 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:
- writeFile shows a diff by default. User must approve changes.
autoApproveonly when trust level is AUTONOMOUS for this domain. - runCommand always shows the terminal. User sees what's running. No hidden execution. This is a security AND trust decision.
- searchCode uses VSCode's file API, not shell
grep. Works on all platforms, respects.gitignore, handles large workspaces.
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)
- IdeToolService: readFile, writeFile, listFiles, searchCode
- ContextProviderService: active file, selection, diagnostics
- Message protocol: 9 new types
- Main panel handlers
Phase 2: Agent Loop (2 sessions)
- AgentSessionService: SSE streaming + tool execution loop
- Backend:
/engine/agent-sessionSSE endpoint (new controller on vitto-api) - Backend: Tool call → extension execution → result POST flow
Phase 3: Chat UI (1-2 sessions)
- agent-chat-view.ts: Lit component with streaming messages
- Tool use cards (file read, command run, diff preview)
- Input bar with Shift+Enter multiline
- Register as new tab in MCP dashboard
Phase 4: Commands & Polish (1-2 sessions)
- Keybindings: Cmd+Shift+V (open agent), Cmd+Shift+A (ask about selection)
- Context menu integration
- DiffContentProvider for write previews
- Destructive command detection
- Rate limiting enforcement
- Terminal output capture fallbacks
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
- Not replacing the iframe chat. The Angular Vitto chat stays for Observatory, brain visualization, rich components, multi-channel messaging. Agent Mode is for IDE work specifically.
- Not running LLMs locally. The extension is a thin client. AI runs on Vitto backend.
- Not building a full terminal emulator. We show commands in VSCode's native terminal. No custom terminal rendering.
- Not supporting multi-root workspaces. First workspace folder is the root. Multi-root is a future enhancement.
- Not implementing browser automation. That's computer-use on VM tier (S240). This spec is about IDE-local operations only.
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
- Spec read and understood
- Backend vitto-api running on port 3003
- Extension can be built:
cd apps/vitto-vscode-extension && node esbuild.mjs - Extension can be packaged:
npm run package - Extension can be installed:
cursor --install-extension releases/vitto-X.Y.Z.vsix - Phase 1 complete → verify: file read/write from webview works
- Phase 2 complete → verify: send message → SSE stream → tool call → result → continue
- Phase 3 complete → verify: chat UI renders, streams, shows tool cards
- Phase 4 complete → verify: Cmd+Shift+V opens agent, context menu works
- Always bump version in package.json before building
- Always build + package + install after every change
- Always reload Cursor window after install
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.