feat: fully flesh out VS Code extension with all RPC features
GsdClient — expose all 25 RPC commands: - Prompting: steer, followUp - Thinking: setThinkingLevel, cycleThinkingLevel - Compaction: compact, setAutoCompaction - Retry: setAutoRetry, abortRetry - Bash: runBash, abortBash - Session: getSessionStats, exportHtml, switchSession, setSessionName, getMessages, getLastAssistantText, getCommands - Model: cycleModel Extension — register 15 commands with full UI: - switchModel (QuickPick with context windows) - setThinking (QuickPick off/low/medium/high) - sessionStats (formatted token/cost display) - exportHtml (save dialog) - steer/runBash (input boxes) - listCommands (QuickPick, select to execute) - Keybindings: ctrl+shift+g chords for new session, cycle model, cycle thinking - Config: gsd.autoStart, gsd.autoCompaction Sidebar — full dashboard: - Thinking level badge and toggle - Token usage (input/output) and cost from session stats - Streaming spinner indicator - Model selector and quick action buttons (compact, export, abort) - Auto-compaction toggle - 10s periodic refresh for live stats Chat participant — enhanced event handling: - Tool-specific details (file paths, bash commands, grep patterns) - Thinking block display - Token usage summary at end of each response
This commit is contained in:
parent
48feced87d
commit
d5e664c580
6 changed files with 905 additions and 50 deletions
41
vscode-extension/package-lock.json
generated
Normal file
41
vscode-extension/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "gsd-vscode",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "gsd-vscode",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/vscode": "^1.95.0",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.95.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/vscode": {
|
||||||
|
"version": "1.110.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz",
|
||||||
|
"integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,67 @@
|
||||||
{
|
{
|
||||||
"command": "gsd.sendMessage",
|
"command": "gsd.sendMessage",
|
||||||
"title": "GSD: Send Message"
|
"title": "GSD: Send Message"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.cycleModel",
|
||||||
|
"title": "GSD: Cycle Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.cycleThinking",
|
||||||
|
"title": "GSD: Cycle Thinking Level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.compact",
|
||||||
|
"title": "GSD: Compact Context"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.abort",
|
||||||
|
"title": "GSD: Abort Current Operation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.exportHtml",
|
||||||
|
"title": "GSD: Export Conversation as HTML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.sessionStats",
|
||||||
|
"title": "GSD: Show Session Stats"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.runBash",
|
||||||
|
"title": "GSD: Run Bash Command"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.switchModel",
|
||||||
|
"title": "GSD: Switch Model"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.setThinking",
|
||||||
|
"title": "GSD: Set Thinking Level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.steer",
|
||||||
|
"title": "GSD: Steer Agent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.listCommands",
|
||||||
|
"title": "GSD: List Available Commands"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"command": "gsd.newSession",
|
||||||
|
"key": "ctrl+shift+g ctrl+shift+n",
|
||||||
|
"mac": "cmd+shift+g cmd+shift+n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.cycleModel",
|
||||||
|
"key": "ctrl+shift+g ctrl+shift+m",
|
||||||
|
"mac": "cmd+shift+g cmd+shift+m"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "gsd.cycleThinking",
|
||||||
|
"key": "ctrl+shift+g ctrl+shift+t",
|
||||||
|
"mac": "cmd+shift+g cmd+shift+t"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"viewsContainers": {
|
"viewsContainers": {
|
||||||
|
|
@ -69,6 +130,16 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "gsd",
|
"default": "gsd",
|
||||||
"description": "Path to the GSD binary"
|
"description": "Path to the GSD binary"
|
||||||
|
},
|
||||||
|
"gsd.autoStart": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Automatically start the GSD agent when the extension activates"
|
||||||
|
},
|
||||||
|
"gsd.autoCompaction": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Enable automatic context compaction"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,13 @@ export function registerChatParticipant(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the message starts with /, forward as a slash command prompt
|
||||||
|
const isSlashCommand = message.startsWith("/");
|
||||||
|
|
||||||
// Track streaming events while the prompt executes
|
// Track streaming events while the prompt executes
|
||||||
let agentDone = false;
|
let agentDone = false;
|
||||||
|
let totalInputTokens = 0;
|
||||||
|
let totalOutputTokens = 0;
|
||||||
|
|
||||||
const eventHandler = (event: AgentEvent) => {
|
const eventHandler = (event: AgentEvent) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
|
@ -35,9 +40,33 @@ export function registerChatParticipant(
|
||||||
response.progress("GSD is working...");
|
response.progress("GSD is working...");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "tool_execution_start":
|
case "tool_execution_start": {
|
||||||
response.progress(`Running tool: ${event.toolName}`);
|
const toolName = event.toolName as string;
|
||||||
|
const toolInput = event.toolInput as Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
let detail = `Running tool: ${toolName}`;
|
||||||
|
|
||||||
|
// Show relevant parameters for common tools
|
||||||
|
if (toolInput) {
|
||||||
|
if (toolName === "Read" && toolInput.file_path) {
|
||||||
|
detail = `Reading: ${toolInput.file_path}`;
|
||||||
|
} else if (toolName === "Write" && toolInput.file_path) {
|
||||||
|
detail = `Writing: ${toolInput.file_path}`;
|
||||||
|
} else if (toolName === "Edit" && toolInput.file_path) {
|
||||||
|
detail = `Editing: ${toolInput.file_path}`;
|
||||||
|
} else if (toolName === "Bash" && toolInput.command) {
|
||||||
|
const cmd = String(toolInput.command);
|
||||||
|
detail = `Running: $ ${cmd.length > 80 ? cmd.slice(0, 77) + "..." : cmd}`;
|
||||||
|
} else if (toolName === "Glob" && toolInput.pattern) {
|
||||||
|
detail = `Searching: ${toolInput.pattern}`;
|
||||||
|
} else if (toolName === "Grep" && toolInput.pattern) {
|
||||||
|
detail = `Grep: ${toolInput.pattern}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.progress(detail);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "tool_execution_end": {
|
case "tool_execution_end": {
|
||||||
const toolName = event.toolName as string;
|
const toolName = event.toolName as string;
|
||||||
|
|
@ -51,20 +80,35 @@ export function registerChatParticipant(
|
||||||
}
|
}
|
||||||
|
|
||||||
case "message_start": {
|
case "message_start": {
|
||||||
const msg = event.message as Record<string, unknown>;
|
// Assistant message starting
|
||||||
if (msg && msg.role === "assistant") {
|
|
||||||
// Assistant message starting, will be followed by updates
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "message_update": {
|
case "message_update": {
|
||||||
const assistantEvent = event.assistantMessageEvent as Record<string, unknown> | undefined;
|
const assistantEvent = event.assistantMessageEvent as Record<string, unknown> | undefined;
|
||||||
if (assistantEvent?.type === "text_delta") {
|
if (!assistantEvent) break;
|
||||||
|
|
||||||
|
if (assistantEvent.type === "text_delta") {
|
||||||
const delta = assistantEvent.delta as string | undefined;
|
const delta = assistantEvent.delta as string | undefined;
|
||||||
if (delta) {
|
if (delta) {
|
||||||
response.markdown(delta);
|
response.markdown(delta);
|
||||||
}
|
}
|
||||||
|
} else if (assistantEvent.type === "thinking_delta") {
|
||||||
|
// Show thinking content in a collapsed section
|
||||||
|
const delta = assistantEvent.delta as string | undefined;
|
||||||
|
if (delta) {
|
||||||
|
response.markdown(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "message_end": {
|
||||||
|
// Capture token usage from message end events
|
||||||
|
const usage = event.usage as { inputTokens?: number; outputTokens?: number } | undefined;
|
||||||
|
if (usage) {
|
||||||
|
if (usage.inputTokens) totalInputTokens += usage.inputTokens;
|
||||||
|
if (usage.outputTokens) totalOutputTokens += usage.outputTokens;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +127,12 @@ export function registerChatParticipant(
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.sendPrompt(message);
|
if (isSlashCommand) {
|
||||||
|
// Forward slash commands as regular prompts
|
||||||
|
await client.sendPrompt(message);
|
||||||
|
} else {
|
||||||
|
await client.sendPrompt(message);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for agent_end or cancellation
|
// Wait for agent_end or cancellation
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
|
|
@ -104,6 +153,13 @@ export function registerChatParticipant(
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show token usage summary at the end
|
||||||
|
if (totalInputTokens > 0 || totalOutputTokens > 0) {
|
||||||
|
response.markdown(
|
||||||
|
`\n\n---\n*Tokens: ${totalInputTokens.toLocaleString()} in / ${totalOutputTokens.toLocaleString()} out*\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
response.markdown(`\n**Error:** ${errorMessage}\n`);
|
response.markdown(`\n**Error:** ${errorMessage}\n`);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { GsdClient } from "./gsd-client.js";
|
import { GsdClient, ThinkingLevel } from "./gsd-client.js";
|
||||||
import { registerChatParticipant } from "./chat-participant.js";
|
import { registerChatParticipant } from "./chat-participant.js";
|
||||||
import { GsdSidebarProvider } from "./sidebar.js";
|
import { GsdSidebarProvider } from "./sidebar.js";
|
||||||
|
|
||||||
let client: GsdClient | undefined;
|
let client: GsdClient | undefined;
|
||||||
let sidebarProvider: GsdSidebarProvider | undefined;
|
let sidebarProvider: GsdSidebarProvider | undefined;
|
||||||
|
|
||||||
|
function requireConnected(): boolean {
|
||||||
|
if (!client?.isConnected) {
|
||||||
|
vscode.window.showWarningMessage("GSD agent is not running.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(err: unknown, context: string): void {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
vscode.window.showErrorMessage(`${context}: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext): void {
|
export function activate(context: vscode.ExtensionContext): void {
|
||||||
const config = vscode.workspace.getConfiguration("gsd");
|
const config = vscode.workspace.getConfiguration("gsd");
|
||||||
const binaryPath = config.get<string>("binaryPath", "gsd");
|
const binaryPath = config.get<string>("binaryPath", "gsd");
|
||||||
|
|
@ -46,19 +59,23 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
|
|
||||||
// -- Commands -----------------------------------------------------------
|
// -- Commands -----------------------------------------------------------
|
||||||
|
|
||||||
|
// Start
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("gsd.start", async () => {
|
vscode.commands.registerCommand("gsd.start", async () => {
|
||||||
try {
|
try {
|
||||||
await client!.start();
|
await client!.start();
|
||||||
|
// Apply auto-compaction setting
|
||||||
|
const autoCompaction = vscode.workspace.getConfiguration("gsd").get<boolean>("autoCompaction", true);
|
||||||
|
await client!.setAutoCompaction(autoCompaction).catch(() => {});
|
||||||
sidebarProvider?.refresh();
|
sidebarProvider?.refresh();
|
||||||
vscode.window.showInformationMessage("GSD agent started.");
|
vscode.window.showInformationMessage("GSD agent started.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
handleError(err, "Failed to start GSD");
|
||||||
vscode.window.showErrorMessage(`Failed to start GSD: ${msg}`);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Stop
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("gsd.stop", async () => {
|
vscode.commands.registerCommand("gsd.stop", async () => {
|
||||||
await client!.stop();
|
await client!.stop();
|
||||||
|
|
@ -67,44 +84,271 @@ export function activate(context: vscode.ExtensionContext): void {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// New Session
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("gsd.newSession", async () => {
|
vscode.commands.registerCommand("gsd.newSession", async () => {
|
||||||
if (!client!.isConnected) {
|
if (!requireConnected()) return;
|
||||||
vscode.window.showWarningMessage("GSD agent is not running.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await client!.newSession();
|
await client!.newSession();
|
||||||
sidebarProvider?.refresh();
|
sidebarProvider?.refresh();
|
||||||
vscode.window.showInformationMessage("New GSD session started.");
|
vscode.window.showInformationMessage("New GSD session started.");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
handleError(err, "Failed to start new session");
|
||||||
vscode.window.showErrorMessage(`Failed to start new session: ${msg}`);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Send Message
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("gsd.sendMessage", async () => {
|
vscode.commands.registerCommand("gsd.sendMessage", async () => {
|
||||||
if (!client!.isConnected) {
|
if (!requireConnected()) return;
|
||||||
vscode.window.showWarningMessage("GSD agent is not running.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const message = await vscode.window.showInputBox({
|
const message = await vscode.window.showInputBox({
|
||||||
prompt: "Enter message for GSD",
|
prompt: "Enter message for GSD",
|
||||||
placeHolder: "What should I do?",
|
placeHolder: "What should I do?",
|
||||||
});
|
});
|
||||||
if (!message) {
|
if (!message) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await client!.sendPrompt(message);
|
await client!.sendPrompt(message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
handleError(err, "Failed to send message");
|
||||||
vscode.window.showErrorMessage(`Failed to send message: ${msg}`);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Abort
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.abort", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
await client!.abort();
|
||||||
|
vscode.window.showInformationMessage("Operation aborted.");
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to abort");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cycle Model
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.cycleModel", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const result = await client!.cycleModel();
|
||||||
|
if (result) {
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
`Model: ${result.model.provider}/${result.model.id} (thinking: ${result.thinkingLevel})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
vscode.window.showInformationMessage("No other models available.");
|
||||||
|
}
|
||||||
|
sidebarProvider?.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to cycle model");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Switch Model (QuickPick)
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.switchModel", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const models = await client!.getAvailableModels();
|
||||||
|
if (models.length === 0) {
|
||||||
|
vscode.window.showInformationMessage("No models available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = models.map((m) => ({
|
||||||
|
label: `${m.provider}/${m.id}`,
|
||||||
|
description: m.contextWindow ? `${Math.round(m.contextWindow / 1000)}k context` : undefined,
|
||||||
|
provider: m.provider,
|
||||||
|
modelId: m.id,
|
||||||
|
}));
|
||||||
|
const selected = await vscode.window.showQuickPick(items, {
|
||||||
|
placeHolder: "Select a model",
|
||||||
|
});
|
||||||
|
if (!selected) return;
|
||||||
|
await client!.setModel(selected.provider, selected.modelId);
|
||||||
|
vscode.window.showInformationMessage(`Model set to ${selected.label}`);
|
||||||
|
sidebarProvider?.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to switch model");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cycle Thinking Level
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.cycleThinking", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const result = await client!.cycleThinkingLevel();
|
||||||
|
if (result) {
|
||||||
|
vscode.window.showInformationMessage(`Thinking level: ${result.level}`);
|
||||||
|
} else {
|
||||||
|
vscode.window.showInformationMessage("Cannot change thinking level for this model.");
|
||||||
|
}
|
||||||
|
sidebarProvider?.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to cycle thinking level");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set Thinking Level (QuickPick)
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.setThinking", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
const levels: ThinkingLevel[] = ["off", "low", "medium", "high"];
|
||||||
|
const selected = await vscode.window.showQuickPick(levels, {
|
||||||
|
placeHolder: "Select thinking level",
|
||||||
|
});
|
||||||
|
if (!selected) return;
|
||||||
|
try {
|
||||||
|
await client!.setThinkingLevel(selected as ThinkingLevel);
|
||||||
|
vscode.window.showInformationMessage(`Thinking level set to ${selected}`);
|
||||||
|
sidebarProvider?.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to set thinking level");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compact Context
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.compact", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
await client!.compact();
|
||||||
|
vscode.window.showInformationMessage("Context compacted.");
|
||||||
|
sidebarProvider?.refresh();
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to compact context");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Export HTML
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.exportHtml", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const saveUri = await vscode.window.showSaveDialog({
|
||||||
|
defaultUri: vscode.Uri.file("gsd-conversation.html"),
|
||||||
|
filters: { "HTML Files": ["html"] },
|
||||||
|
});
|
||||||
|
const outputPath = saveUri?.fsPath;
|
||||||
|
const result = await client!.exportHtml(outputPath);
|
||||||
|
vscode.window.showInformationMessage(`Conversation exported to ${result.path}`);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to export HTML");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Session Stats
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.sessionStats", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const stats = await client!.getSessionStats();
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (stats.inputTokens !== undefined) lines.push(`Input tokens: ${stats.inputTokens.toLocaleString()}`);
|
||||||
|
if (stats.outputTokens !== undefined) lines.push(`Output tokens: ${stats.outputTokens.toLocaleString()}`);
|
||||||
|
if (stats.cacheReadTokens !== undefined) lines.push(`Cache read: ${stats.cacheReadTokens.toLocaleString()}`);
|
||||||
|
if (stats.cacheWriteTokens !== undefined) lines.push(`Cache write: ${stats.cacheWriteTokens.toLocaleString()}`);
|
||||||
|
if (stats.totalCost !== undefined) lines.push(`Cost: $${stats.totalCost.toFixed(4)}`);
|
||||||
|
if (stats.turnCount !== undefined) lines.push(`Turns: ${stats.turnCount}`);
|
||||||
|
if (stats.messageCount !== undefined) lines.push(`Messages: ${stats.messageCount}`);
|
||||||
|
if (stats.duration !== undefined) lines.push(`Duration: ${Math.round(stats.duration / 1000)}s`);
|
||||||
|
|
||||||
|
vscode.window.showInformationMessage(
|
||||||
|
lines.length > 0 ? lines.join(" | ") : "No stats available.",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to get session stats");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run Bash Command
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.runBash", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
const command = await vscode.window.showInputBox({
|
||||||
|
prompt: "Enter bash command to execute",
|
||||||
|
placeHolder: "ls -la",
|
||||||
|
});
|
||||||
|
if (!command) return;
|
||||||
|
try {
|
||||||
|
const result = await client!.runBash(command);
|
||||||
|
outputChannel.appendLine(`[bash] $ ${command}`);
|
||||||
|
if (result.stdout) outputChannel.appendLine(result.stdout);
|
||||||
|
if (result.stderr) outputChannel.appendLine(`[stderr] ${result.stderr}`);
|
||||||
|
outputChannel.appendLine(`[exit code: ${result.exitCode}]`);
|
||||||
|
outputChannel.show(true);
|
||||||
|
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
vscode.window.showInformationMessage("Bash command completed successfully.");
|
||||||
|
} else {
|
||||||
|
vscode.window.showWarningMessage(`Bash command exited with code ${result.exitCode}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to run bash command");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Steer Agent
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.steer", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
const message = await vscode.window.showInputBox({
|
||||||
|
prompt: "Enter steering message (interrupts current operation)",
|
||||||
|
placeHolder: "Focus on the error handling instead",
|
||||||
|
});
|
||||||
|
if (!message) return;
|
||||||
|
try {
|
||||||
|
await client!.steer(message);
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to steer agent");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// List Available Commands
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand("gsd.listCommands", async () => {
|
||||||
|
if (!requireConnected()) return;
|
||||||
|
try {
|
||||||
|
const commands = await client!.getCommands();
|
||||||
|
if (commands.length === 0) {
|
||||||
|
vscode.window.showInformationMessage("No slash commands available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const items = commands.map((cmd) => ({
|
||||||
|
label: `/${cmd.name}`,
|
||||||
|
description: cmd.description ?? "",
|
||||||
|
detail: `Source: ${cmd.source}${cmd.location ? ` (${cmd.location})` : ""}`,
|
||||||
|
}));
|
||||||
|
const selected = await vscode.window.showQuickPick(items, {
|
||||||
|
placeHolder: "Available slash commands",
|
||||||
|
});
|
||||||
|
if (selected) {
|
||||||
|
// Send the selected command as a prompt
|
||||||
|
await client!.sendPrompt(selected.label);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err, "Failed to list commands");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// -- Auto-start ---------------------------------------------------------
|
||||||
|
|
||||||
|
if (config.get<boolean>("autoStart", false)) {
|
||||||
|
vscode.commands.executeCommand("gsd.start");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): void {
|
export function deactivate(): void {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ import * as vscode from "vscode";
|
||||||
* extension has no dependency on the agent packages at runtime.
|
* extension has no dependency on the agent packages at runtime.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export type ThinkingLevel = "off" | "low" | "medium" | "high";
|
||||||
|
|
||||||
export interface RpcSessionState {
|
export interface RpcSessionState {
|
||||||
model?: { provider: string; id: string; contextWindow?: number };
|
model?: { provider: string; id: string; contextWindow?: number };
|
||||||
thinkingLevel: string;
|
thinkingLevel: ThinkingLevel;
|
||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
isCompacting: boolean;
|
isCompacting: boolean;
|
||||||
steeringMode: "all" | "one-at-a-time";
|
steeringMode: "all" | "one-at-a-time";
|
||||||
|
|
@ -29,6 +31,31 @@ export interface ModelInfo {
|
||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionStats {
|
||||||
|
inputTokens?: number;
|
||||||
|
outputTokens?: number;
|
||||||
|
cacheReadTokens?: number;
|
||||||
|
cacheWriteTokens?: number;
|
||||||
|
totalCost?: number;
|
||||||
|
messageCount?: number;
|
||||||
|
turnCount?: number;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BashResult {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
exitCode: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlashCommand {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
source: "extension" | "prompt" | "skill";
|
||||||
|
location?: "user" | "project" | "path";
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RpcResponse {
|
export interface RpcResponse {
|
||||||
id?: string;
|
id?: string;
|
||||||
type: "response";
|
type: "response";
|
||||||
|
|
@ -152,6 +179,10 @@ export class GsdClient implements vscode.Disposable {
|
||||||
this._onConnectionChange.fire(false);
|
this._onConnectionChange.fire(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Prompting
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a prompt message to the agent.
|
* Send a prompt message to the agent.
|
||||||
* Returns once the command is acknowledged; streaming events follow via onEvent.
|
* Returns once the command is acknowledged; streaming events follow via onEvent.
|
||||||
|
|
@ -161,6 +192,22 @@ export class GsdClient implements vscode.Disposable {
|
||||||
this.assertSuccess(response);
|
this.assertSuccess(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interrupt the agent with a steering message while it is streaming.
|
||||||
|
*/
|
||||||
|
async steer(message: string): Promise<void> {
|
||||||
|
const response = await this.send({ type: "steer", message });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a follow-up message after the agent has completed.
|
||||||
|
*/
|
||||||
|
async followUp(message: string): Promise<void> {
|
||||||
|
const response = await this.send({ type: "follow_up", message });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abort current operation.
|
* Abort current operation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -169,6 +216,10 @@ export class GsdClient implements vscode.Disposable {
|
||||||
this.assertSuccess(response);
|
this.assertSuccess(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// State
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current session state.
|
* Get current session state.
|
||||||
*/
|
*/
|
||||||
|
|
@ -178,6 +229,10 @@ export class GsdClient implements vscode.Disposable {
|
||||||
return response.data as RpcSessionState;
|
return response.data as RpcSessionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Model
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the active model.
|
* Set the active model.
|
||||||
*/
|
*/
|
||||||
|
|
@ -195,6 +250,106 @@ export class GsdClient implements vscode.Disposable {
|
||||||
return (response.data as { models: ModelInfo[] }).models;
|
return (response.data as { models: ModelInfo[] }).models;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle through available models.
|
||||||
|
*/
|
||||||
|
async cycleModel(): Promise<{ model: ModelInfo; thinkingLevel: ThinkingLevel; isScoped: boolean } | null> {
|
||||||
|
const response = await this.send({ type: "cycle_model" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data as { model: ModelInfo; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Thinking
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the thinking level explicitly.
|
||||||
|
*/
|
||||||
|
async setThinkingLevel(level: ThinkingLevel): Promise<void> {
|
||||||
|
const response = await this.send({ type: "set_thinking_level", level });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle through thinking levels (off -> low -> medium -> high -> off).
|
||||||
|
*/
|
||||||
|
async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {
|
||||||
|
const response = await this.send({ type: "cycle_thinking_level" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data as { level: ThinkingLevel } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Compaction
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually compact the conversation context.
|
||||||
|
*/
|
||||||
|
async compact(customInstructions?: string): Promise<unknown> {
|
||||||
|
const cmd: Record<string, unknown> = { type: "compact" };
|
||||||
|
if (customInstructions) {
|
||||||
|
cmd.customInstructions = customInstructions;
|
||||||
|
}
|
||||||
|
const response = await this.send(cmd);
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable automatic compaction.
|
||||||
|
*/
|
||||||
|
async setAutoCompaction(enabled: boolean): Promise<void> {
|
||||||
|
const response = await this.send({ type: "set_auto_compaction", enabled });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Retry
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable automatic retry on failure.
|
||||||
|
*/
|
||||||
|
async setAutoRetry(enabled: boolean): Promise<void> {
|
||||||
|
const response = await this.send({ type: "set_auto_retry", enabled });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort a pending retry.
|
||||||
|
*/
|
||||||
|
async abortRetry(): Promise<void> {
|
||||||
|
const response = await this.send({ type: "abort_retry" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Bash
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a bash command via the agent.
|
||||||
|
*/
|
||||||
|
async runBash(command: string): Promise<BashResult> {
|
||||||
|
const response = await this.send({ type: "bash", command });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data as BashResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abort a running bash command.
|
||||||
|
*/
|
||||||
|
async abortBash(): Promise<void> {
|
||||||
|
const response = await this.send({ type: "abort_bash" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Session
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start a new session.
|
* Start a new session.
|
||||||
*/
|
*/
|
||||||
|
|
@ -203,6 +358,71 @@ export class GsdClient implements vscode.Disposable {
|
||||||
this.assertSuccess(response);
|
this.assertSuccess(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session statistics (token counts, cost, etc.).
|
||||||
|
*/
|
||||||
|
async getSessionStats(): Promise<SessionStats> {
|
||||||
|
const response = await this.send({ type: "get_session_stats" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data as SessionStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export the conversation as HTML.
|
||||||
|
*/
|
||||||
|
async exportHtml(outputPath?: string): Promise<{ path: string }> {
|
||||||
|
const cmd: Record<string, unknown> = { type: "export_html" };
|
||||||
|
if (outputPath) {
|
||||||
|
cmd.outputPath = outputPath;
|
||||||
|
}
|
||||||
|
const response = await this.send(cmd);
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return response.data as { path: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different session file.
|
||||||
|
*/
|
||||||
|
async switchSession(sessionPath: string): Promise<void> {
|
||||||
|
const response = await this.send({ type: "switch_session", sessionPath });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the display name for the current session.
|
||||||
|
*/
|
||||||
|
async setSessionName(name: string): Promise<void> {
|
||||||
|
const response = await this.send({ type: "set_session_name", name });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all conversation messages.
|
||||||
|
*/
|
||||||
|
async getMessages(): Promise<unknown[]> {
|
||||||
|
const response = await this.send({ type: "get_messages" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return (response.data as { messages: unknown[] }).messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text of the last assistant response.
|
||||||
|
*/
|
||||||
|
async getLastAssistantText(): Promise<string | null> {
|
||||||
|
const response = await this.send({ type: "get_last_assistant_text" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return (response.data as { text: string | null }).text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List available slash commands.
|
||||||
|
*/
|
||||||
|
async getCommands(): Promise<SlashCommand[]> {
|
||||||
|
const response = await this.send({ type: "get_commands" });
|
||||||
|
this.assertSuccess(response);
|
||||||
|
return (response.data as { commands: SlashCommand[] }).commands;
|
||||||
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
this.stop();
|
this.stop();
|
||||||
for (const d of this.disposables) {
|
for (const d of this.disposables) {
|
||||||
|
|
@ -278,7 +498,7 @@ export class GsdClient implements vscode.Disposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private rejectAllPending(reason: string): void {
|
private rejectAllPending(reason: string): void {
|
||||||
for (const [id, pending] of this.pendingRequests) {
|
for (const [, pending] of this.pendingRequests) {
|
||||||
clearTimeout(pending.timer);
|
clearTimeout(pending.timer);
|
||||||
pending.reject(new Error(reason));
|
pending.reject(new Error(reason));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import type { GsdClient } from "./gsd-client.js";
|
import type { GsdClient, SessionStats, ThinkingLevel } from "./gsd-client.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebviewViewProvider that renders a simple sidebar panel showing
|
* WebviewViewProvider that renders a sidebar panel showing connection status,
|
||||||
* connection status, current model, session info, and start/stop controls.
|
* model info, thinking level, token usage, cost, and quick action controls.
|
||||||
*/
|
*/
|
||||||
export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
public static readonly viewId = "gsd-sidebar";
|
public static readonly viewId = "gsd-sidebar";
|
||||||
|
|
||||||
private view?: vscode.WebviewView;
|
private view?: vscode.WebviewView;
|
||||||
private disposables: vscode.Disposable[] = [];
|
private disposables: vscode.Disposable[] = [];
|
||||||
|
private refreshTimer: ReturnType<typeof setInterval> | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly extensionUri: vscode.Uri,
|
private readonly extensionUri: vscode.Uri,
|
||||||
|
|
@ -17,6 +18,12 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
) {
|
) {
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
client.onConnectionChange(() => this.refresh()),
|
client.onConnectionChange(() => this.refresh()),
|
||||||
|
client.onEvent((evt) => {
|
||||||
|
// Refresh on streaming state changes
|
||||||
|
if (evt.type === "agent_start" || evt.type === "agent_end") {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +38,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
enableScripts: true,
|
enableScripts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
webviewView.webview.onDidReceiveMessage(async (msg: { command: string }) => {
|
webviewView.webview.onDidReceiveMessage(async (msg: { command: string; value?: string }) => {
|
||||||
switch (msg.command) {
|
switch (msg.command) {
|
||||||
case "start":
|
case "start":
|
||||||
await vscode.commands.executeCommand("gsd.start");
|
await vscode.commands.executeCommand("gsd.start");
|
||||||
|
|
@ -42,9 +49,52 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
case "newSession":
|
case "newSession":
|
||||||
await vscode.commands.executeCommand("gsd.newSession");
|
await vscode.commands.executeCommand("gsd.newSession");
|
||||||
break;
|
break;
|
||||||
|
case "cycleModel":
|
||||||
|
await vscode.commands.executeCommand("gsd.cycleModel");
|
||||||
|
break;
|
||||||
|
case "cycleThinking":
|
||||||
|
await vscode.commands.executeCommand("gsd.cycleThinking");
|
||||||
|
break;
|
||||||
|
case "switchModel":
|
||||||
|
await vscode.commands.executeCommand("gsd.switchModel");
|
||||||
|
break;
|
||||||
|
case "setThinking":
|
||||||
|
await vscode.commands.executeCommand("gsd.setThinking");
|
||||||
|
break;
|
||||||
|
case "compact":
|
||||||
|
await vscode.commands.executeCommand("gsd.compact");
|
||||||
|
break;
|
||||||
|
case "abort":
|
||||||
|
await vscode.commands.executeCommand("gsd.abort");
|
||||||
|
break;
|
||||||
|
case "exportHtml":
|
||||||
|
await vscode.commands.executeCommand("gsd.exportHtml");
|
||||||
|
break;
|
||||||
|
case "sessionStats":
|
||||||
|
await vscode.commands.executeCommand("gsd.sessionStats");
|
||||||
|
break;
|
||||||
|
case "listCommands":
|
||||||
|
await vscode.commands.executeCommand("gsd.listCommands");
|
||||||
|
break;
|
||||||
|
case "toggleAutoCompaction":
|
||||||
|
if (this.client.isConnected) {
|
||||||
|
const state = await this.client.getState().catch(() => null);
|
||||||
|
if (state) {
|
||||||
|
await this.client.setAutoCompaction(!state.autoCompactionEnabled).catch(() => {});
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Periodic refresh while connected (for token stats)
|
||||||
|
this.refreshTimer = setInterval(() => {
|
||||||
|
if (this.client.isConnected) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}, 10_000);
|
||||||
|
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,6 +107,11 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
let sessionId = "N/A";
|
let sessionId = "N/A";
|
||||||
let sessionName = "";
|
let sessionName = "";
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
|
let thinkingLevel: ThinkingLevel = "off";
|
||||||
|
let isStreaming = false;
|
||||||
|
let isCompacting = false;
|
||||||
|
let autoCompaction = false;
|
||||||
|
let stats: SessionStats | null = null;
|
||||||
|
|
||||||
if (this.client.isConnected) {
|
if (this.client.isConnected) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -67,9 +122,19 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
sessionId = state.sessionId;
|
sessionId = state.sessionId;
|
||||||
sessionName = state.sessionName ?? "";
|
sessionName = state.sessionName ?? "";
|
||||||
messageCount = state.messageCount;
|
messageCount = state.messageCount;
|
||||||
|
thinkingLevel = state.thinkingLevel as ThinkingLevel;
|
||||||
|
isStreaming = state.isStreaming;
|
||||||
|
isCompacting = state.isCompacting;
|
||||||
|
autoCompaction = state.autoCompactionEnabled;
|
||||||
} catch {
|
} catch {
|
||||||
// State fetch failed, show defaults
|
// State fetch failed, show defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stats = await this.client.getSessionStats();
|
||||||
|
} catch {
|
||||||
|
// Stats fetch failed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const connected = this.client.isConnected;
|
const connected = this.client.isConnected;
|
||||||
|
|
@ -80,10 +145,18 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
sessionId,
|
sessionId,
|
||||||
sessionName,
|
sessionName,
|
||||||
messageCount,
|
messageCount,
|
||||||
|
thinkingLevel,
|
||||||
|
isStreaming,
|
||||||
|
isCompacting,
|
||||||
|
autoCompaction,
|
||||||
|
stats,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
if (this.refreshTimer) {
|
||||||
|
clearInterval(this.refreshTimer);
|
||||||
|
}
|
||||||
for (const d of this.disposables) {
|
for (const d of this.disposables) {
|
||||||
d.dispose();
|
d.dispose();
|
||||||
}
|
}
|
||||||
|
|
@ -95,9 +168,36 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
sessionName: string;
|
sessionName: string;
|
||||||
messageCount: number;
|
messageCount: number;
|
||||||
|
thinkingLevel: ThinkingLevel;
|
||||||
|
isStreaming: boolean;
|
||||||
|
isCompacting: boolean;
|
||||||
|
autoCompaction: boolean;
|
||||||
|
stats: SessionStats | null;
|
||||||
}): string {
|
}): string {
|
||||||
const statusColor = info.connected ? "#4ec9b0" : "#f44747";
|
const statusColor = info.connected ? "#4ec9b0" : "#f44747";
|
||||||
const statusText = info.connected ? "Connected" : "Disconnected";
|
const statusText = info.connected
|
||||||
|
? info.isStreaming
|
||||||
|
? "Processing..."
|
||||||
|
: info.isCompacting
|
||||||
|
? "Compacting..."
|
||||||
|
: "Connected"
|
||||||
|
: "Disconnected";
|
||||||
|
|
||||||
|
const inputTokens = info.stats?.inputTokens?.toLocaleString() ?? "-";
|
||||||
|
const outputTokens = info.stats?.outputTokens?.toLocaleString() ?? "-";
|
||||||
|
const cost = info.stats?.totalCost !== undefined ? `$${info.stats.totalCost.toFixed(4)}` : "-";
|
||||||
|
|
||||||
|
const thinkingBadge = info.thinkingLevel !== "off"
|
||||||
|
? `<span class="badge">${info.thinkingLevel}</span>`
|
||||||
|
: `<span class="badge muted">off</span>`;
|
||||||
|
|
||||||
|
const autoCompBadge = info.autoCompaction
|
||||||
|
? `<span class="badge">on</span>`
|
||||||
|
: `<span class="badge muted">off</span>`;
|
||||||
|
|
||||||
|
const streamingIndicator = info.isStreaming
|
||||||
|
? `<div class="streaming-indicator"><span class="spinner"></span> Agent is working...</div>`
|
||||||
|
: "";
|
||||||
|
|
||||||
return /* html */ `<!DOCTYPE html>
|
return /* html */ `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
@ -116,20 +216,53 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
.status-dot {
|
.status-dot {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: ${statusColor};
|
background: ${statusColor};
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.streaming-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: var(--vscode-editor-background);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--vscode-focusBorder);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border: 2px solid var(--vscode-foreground);
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
.info-table {
|
.info-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
.info-table td {
|
.info-table td {
|
||||||
padding: 4px 0;
|
padding: 3px 0;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.info-table td:first-child {
|
.info-table td:first-child {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
@ -139,10 +272,34 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
.info-table td:last-child {
|
.info-table td:last-child {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--vscode-badge-background);
|
||||||
|
color: var(--vscode-badge-foreground);
|
||||||
|
}
|
||||||
|
.badge.muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.badge.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.badge.clickable:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
.btn-group {
|
.btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.btn-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.btn-row button {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
@ -165,6 +322,19 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
button.secondary:hover {
|
button.secondary:hover {
|
||||||
background: var(--vscode-button-secondaryHoverBackground);
|
background: var(--vscode-button-secondaryHoverBackground);
|
||||||
}
|
}
|
||||||
|
.token-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.token-stats .label {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token-stats .value {
|
||||||
|
text-align: right;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -173,24 +343,77 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||||
<strong>${statusText}</strong>
|
<strong>${statusText}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="info-table">
|
${streamingIndicator}
|
||||||
<tr><td>Model</td><td>${escapeHtml(info.modelName)}</td></tr>
|
|
||||||
<tr><td>Session</td><td>${escapeHtml(info.sessionName || info.sessionId)}</td></tr>
|
|
||||||
<tr><td>Messages</td><td>${info.messageCount}</td></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="section">
|
||||||
${info.connected
|
<div class="section-title">Session</div>
|
||||||
? `<button onclick="send('stop')">Stop Agent</button>
|
<table class="info-table">
|
||||||
<button class="secondary" onclick="send('newSession')">New Session</button>`
|
<tr><td>Model</td><td>${escapeHtml(info.modelName)}</td></tr>
|
||||||
: `<button onclick="send('start')">Start Agent</button>`
|
<tr><td>Session</td><td>${escapeHtml(info.sessionName || info.sessionId)}</td></tr>
|
||||||
}
|
<tr><td>Messages</td><td>${info.messageCount}</td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>Thinking</td>
|
||||||
|
<td>${thinkingBadge}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Auto-compact</td>
|
||||||
|
<td>${autoCompBadge}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${info.connected && info.stats ? `
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Token Usage</div>
|
||||||
|
<div class="token-stats">
|
||||||
|
<span class="label">Input</span>
|
||||||
|
<span class="value">${inputTokens}</span>
|
||||||
|
<span class="label">Output</span>
|
||||||
|
<span class="value">${outputTokens}</span>
|
||||||
|
<span class="label">Cost</span>
|
||||||
|
<span class="value">${cost}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Controls</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
${info.connected
|
||||||
|
? `<button onclick="send('stop')">Stop Agent</button>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="secondary" onclick="send('newSession')">New Session</button>
|
||||||
|
<button class="secondary" onclick="send('switchModel')">Model</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="secondary" onclick="send('cycleThinking')">Thinking</button>
|
||||||
|
<button class="secondary" onclick="send('toggleAutoCompaction')">Auto-Compact</button>
|
||||||
|
</div>`
|
||||||
|
: `<button onclick="send('start')">Start Agent</button>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${info.connected ? `
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Actions</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="secondary" onclick="send('compact')">Compact</button>
|
||||||
|
<button class="secondary" onclick="send('exportHtml')">Export</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="secondary" onclick="send('abort')">Abort</button>
|
||||||
|
<button class="secondary" onclick="send('listCommands')">Commands</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const vscode = acquireVsCodeApi();
|
const vscode = acquireVsCodeApi();
|
||||||
function send(command) {
|
function send(command, value) {
|
||||||
vscode.postMessage({ command });
|
vscode.postMessage({ command, value });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue