Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions ai-dev-vscode/dist/extension.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions ai-dev-vscode/dist/extension.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ai-dev-vscode/dist/webviews/agents/main.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ai-dev-vscode/dist/webviews/decisions/main.js.map

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions ai-dev-vscode/dist/webviews/logs/main.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions ai-dev-vscode/dist/webviews/logs/main.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ai-dev-vscode/dist/webviews/messages/main.js.map

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions ai-dev-vscode/esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const webviewCtx = await esbuild.context({
'src/webviews/agents/main.tsx',
'src/webviews/messages/main.tsx',
'src/webviews/decisions/main.tsx',
'src/webviews/logs/main.tsx',
],
outdir: 'dist/webviews',
platform: 'browser',
Expand Down
5 changes: 3 additions & 2 deletions ai-dev-vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions ai-dev-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@
"id": "aidev.decisions",
"type": "webview",
"name": "Decisions"
},
{
"id": "aidev.logs",
"type": "webview",
"name": "Logs"
}
]
},
Expand Down
17 changes: 17 additions & 0 deletions ai-dev-vscode/src/BackendProcessManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export type ProcessLike = {
kill(): void;
on(event: 'exit', handler: (code: number | null) => void): void;
on(event: 'error', handler: (err: Error) => void): void;
stdout?: NodeJS.ReadableStream | null;
stderr?: NodeJS.ReadableStream | null;
};

export type Spawner = (
Expand All @@ -21,6 +23,7 @@ export interface BackendProcessManagerOptions {
port: number;
maxAttempts: number;
retryDelayMs: number;
onOutput?: (line: string, source: 'stdout' | 'stderr') => void;
}

export class BackendProcessManager {
Expand Down Expand Up @@ -55,6 +58,20 @@ export class BackendProcessManager {
this._process.on('error', () => {
if (this._state !== 'stopped') this._state = 'stopped';
});

if (this.options.onOutput) {
const { createInterface } = require('readline') as typeof import('readline');
if (this._process.stdout) {
createInterface({ input: this._process.stdout }).on('line', (line: string) => {
this.options.onOutput!(line, 'stdout');
});
}
if (this._process.stderr) {
createInterface({ input: this._process.stderr }).on('line', (line: string) => {
this.options.onOutput!(line, 'stderr');
});
}
}
}

const healthUrl = `http://localhost:${this.options.port}/api/health`;
Expand Down
80 changes: 80 additions & 0 deletions ai-dev-vscode/src/Logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as vscode from 'vscode';

export type LogLevel = 'info' | 'warn' | 'error';

export interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
}

type LogSubscriber = (entry: LogEntry) => void;
type ClearSubscriber = () => void;

const MAX_BUFFER = 1000;

export class Logger implements vscode.Disposable {
private readonly channel: vscode.OutputChannel;
private readonly buffer: LogEntry[] = [];
private readonly subscribers: LogSubscriber[] = [];
private readonly clearSubscribers: ClearSubscriber[] = [];

constructor(channelName: string) {
this.channel = vscode.window.createOutputChannel(channelName);
}

/** Backward-compatible alias for info(). */
appendLine(message: string): void {
this.info(message);
}

info(message: string): void {
this.emit('info', message);
}

warn(message: string): void {
this.emit('warn', message);
}

error(message: string): void {
this.emit('error', message);
}

private emit(level: LogLevel, message: string): void {
const entry: LogEntry = { timestamp: new Date().toISOString(), level, message };
const formatted = `[${entry.timestamp}] [${level.toUpperCase().padEnd(5)}] ${message}`;
this.channel.appendLine(formatted);
this.buffer.push(entry);
if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
for (const sub of this.subscribers) sub(entry);
}

subscribe(fn: LogSubscriber): vscode.Disposable {
this.subscribers.push(fn);
return new vscode.Disposable(() => {
const idx = this.subscribers.indexOf(fn);
if (idx >= 0) this.subscribers.splice(idx, 1);
});
}

onCleared(fn: ClearSubscriber): vscode.Disposable {
this.clearSubscribers.push(fn);
return new vscode.Disposable(() => {
const idx = this.clearSubscribers.indexOf(fn);
if (idx >= 0) this.clearSubscribers.splice(idx, 1);
});
}

getHistory(): LogEntry[] {
return [...this.buffer];
}

clearBuffer(): void {
this.buffer.length = 0;
for (const sub of this.clearSubscribers) sub();
}

dispose(): void {
this.channel.dispose();
}
}
21 changes: 18 additions & 3 deletions ai-dev-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ import { StudioSignalRClient } from './StudioSignalRClient';
import { AgentsPanelProvider } from './panels/AgentsPanelProvider';
import { MessagesPanelProvider } from './panels/MessagesPanelProvider';
import { DecisionsPanelProvider } from './panels/DecisionsPanelProvider';
import { LogsPanelProvider } from './panels/LogsPanelProvider';
import { Logger } from './Logger';
import type { ProjectConfig } from './types';

let backendManager: BackendProcessManager | undefined;
let signalRClient: StudioSignalRClient | undefined;
let statusBar: StatusBarManager | undefined;
export let log: vscode.OutputChannel;
export let log: Logger;

export async function activate(context: vscode.ExtensionContext): Promise<void> {
log = vscode.window.createOutputChannel('AI Dev Studio');
log = new Logger('AI Dev Studio');
context.subscriptions.push(log);
log.appendLine(`Activating — extensionPath: ${context.extensionPath}`);

Expand All @@ -29,11 +31,14 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
const agentsProvider = new AgentsPanelProvider(context.extensionUri);
const messagesProvider = new MessagesPanelProvider(context.extensionUri);
const decisionsProvider = new DecisionsPanelProvider(context.extensionUri);
const logsProvider = new LogsPanelProvider(context.extensionUri, log);

context.subscriptions.push(
vscode.window.registerWebviewViewProvider('aidev.agents', agentsProvider),
vscode.window.registerWebviewViewProvider('aidev.messages', messagesProvider),
vscode.window.registerWebviewViewProvider('aidev.decisions', decisionsProvider),
vscode.window.registerWebviewViewProvider('aidev.logs', logsProvider),
logsProvider,
);

const detector = new WorkspaceDetector(
Expand Down Expand Up @@ -104,7 +109,17 @@ async function connectBackend(
log.appendLine(`Binary: ${binaryPath} (exists: ${fs.existsSync(binaryPath)})`);

backendManager = new BackendProcessManager(
{ binaryPath, port: config.apiPort, maxAttempts: 30, retryDelayMs: 1000 },
{
binaryPath,
port: config.apiPort,
maxAttempts: 30,
retryDelayMs: 1000,
onOutput: (line, source) => {
const msg = `[backend:${source}] ${line}`;
if (source === 'stderr') log.warn(msg);
else log.info(msg);
},
},
(binary, args, options) => spawn(binary, args, {
...options,
stdio: 'pipe',
Expand Down
83 changes: 83 additions & 0 deletions ai-dev-vscode/src/panels/LogsPanelProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import type { Logger } from '../Logger';

export class LogsPanelProvider implements vscode.WebviewViewProvider, vscode.Disposable {
private view?: vscode.WebviewView;
private readonly disposables: vscode.Disposable[] = [];

constructor(
private readonly extensionUri: vscode.Uri,
private readonly logger: Logger,
) {}

resolveWebviewView(webviewView: vscode.WebviewView): void {
this.view = webviewView;

webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [vscode.Uri.joinPath(this.extensionUri, 'dist', 'webviews')],
};
webviewView.webview.html = this.buildHtml(webviewView.webview);

// Send buffered history when the view first opens or becomes visible again.
webviewView.onDidChangeVisibility(() => {
if (webviewView.visible) this.sendHistory();
}, undefined, this.disposables);

this.sendHistory();

// Stream new log entries to the webview.
this.disposables.push(
this.logger.subscribe(entry => {
webviewView.webview.postMessage({ type: 'entry', entry });
}),
);

// When logger buffer is cleared, notify the webview.
this.disposables.push(
this.logger.onCleared(() => {
webviewView.webview.postMessage({ type: 'cleared' });
}),
);

// Handle clear command from the webview.
this.disposables.push(
webviewView.webview.onDidReceiveMessage(msg => {
if (msg.type === 'clear') this.logger.clearBuffer();
}),
);
}

private sendHistory(): void {
this.view?.webview.postMessage({ type: 'history', entries: this.logger.getHistory() });
}

dispose(): void {
for (const d of this.disposables) d.dispose();
}

private buildHtml(webview: vscode.Webview): string {
const nonce = crypto.randomBytes(16).toString('hex');
const scriptUri = webview.asWebviewUri(
vscode.Uri.joinPath(this.extensionUri, 'dist', 'webviews', 'logs/main.js'),
);
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html, body { height: 100%; margin: 0; padding: 0; }
body { font-family: var(--vscode-editor-font-family, monospace); font-size: var(--vscode-editor-font-size, 12px); color: var(--vscode-foreground); background: transparent; }
</style>
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
}
}
Loading
Loading