diff --git a/vscode-extension/CHANGELOG.md b/vscode-extension/CHANGELOG.md index 836dad293..fd532537d 100644 --- a/vscode-extension/CHANGELOG.md +++ b/vscode-extension/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [0.2.0] + +### Added + +- **Activity feed** — real-time TreeView showing tool executions (Read, Write, Edit, Bash, Grep, Glob) with status icons, duration, and click-to-open +- **Workflow controls** — sidebar buttons for Auto, Next, Quick Task, Capture, Status, and Fork that send `/gsd` slash commands +- **Progress notifications** — VS Code notification with cancel button while the agent is working +- **Context window indicator** — color-coded usage bar (green/yellow/red) in sidebar with configurable threshold warnings +- **Session forking** — fork from any message via QuickPick using `get_fork_messages` and `fork` RPC commands +- **Queue mode controls** — toggle steering and follow-up modes (all vs one-at-a-time) from the sidebar +- **Enhanced conversation history** — tool call rendering, collapsible thinking blocks, search/filter, fork-from-here buttons +- **Enhanced code lens** — Refactor, Find Bugs, and Generate Tests actions alongside Ask GSD +- **4 new settings** — `showProgressNotifications`, `activityFeedMaxItems`, `showContextWarning`, `contextWarningThreshold` +- **8 new commands** (33 total) — `clearActivity`, `forkSession`, `toggleSteeringMode`, `toggleFollowUpMode`, `refactorSymbol`, `findBugsSymbol`, `generateTestsSymbol` + +### Changed + +- Sidebar session table now shows steering and follow-up queue mode with clickable toggle badges +- Token usage section includes context window usage bar when model context window is known + ## [0.1.0] Initial release. @@ -7,5 +27,11 @@ Initial release. - Full RPC client — spawns `gsd --mode rpc`, JSON line framing, all RPC commands - Sidebar dashboard — connection status, model info, thinking level, token usage, cost, quick actions - Chat participant — `@gsd` in VS Code Chat with streaming responses -- 15 commands with keyboard shortcuts -- Auto-start and auto-compaction configuration +- File decorations — "G" badge on files modified by the agent +- Bash terminal — pseudoterminal routing agent Bash tool output +- Session tree — browse and switch between session files +- Conversation history — webview panel with full chat log +- Slash command completion — auto-complete for `/gsd` commands in editors +- Code lens — "Ask GSD" above functions and classes in TS/JS/Python/Go/Rust +- 25 commands with 6 keyboard shortcuts +- Auto-start, auto-compaction, and code lens configuration diff --git a/vscode-extension/package.json b/vscode-extension/package.json index be0a26007..8ea2de271 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -1,9 +1,9 @@ { "name": "gsd-2", "displayName": "GSD-2", - "description": "VS Code integration for the GSD-2 coding agent — sidebar dashboard, @gsd chat participant, conversation history, code lens, slash command completion, and 25 commands", + "description": "VS Code integration for the GSD-2 coding agent — sidebar dashboard, @gsd chat participant, activity feed, conversation history, code lens, session forking, slash command completion, workflow controls, and 33 commands", "publisher": "FluxLabs", - "version": "0.1.0", + "version": "0.2.0", "icon": "logo.jpg", "license": "MIT", "repository": { @@ -139,6 +139,35 @@ { "command": "gsd.askAboutSymbol", "title": "GSD: Ask About Symbol" + }, + { + "command": "gsd.clearActivity", + "title": "GSD: Clear Activity Feed", + "icon": "$(clear-all)" + }, + { + "command": "gsd.forkSession", + "title": "GSD: Fork Session" + }, + { + "command": "gsd.toggleSteeringMode", + "title": "GSD: Toggle Steering Mode" + }, + { + "command": "gsd.toggleFollowUpMode", + "title": "GSD: Toggle Follow-Up Mode" + }, + { + "command": "gsd.refactorSymbol", + "title": "GSD: Refactor Symbol" + }, + { + "command": "gsd.findBugsSymbol", + "title": "GSD: Find Bugs in Symbol" + }, + { + "command": "gsd.generateTestsSymbol", + "title": "GSD: Generate Tests for Symbol" } ], "keybindings": [ @@ -192,6 +221,10 @@ { "id": "gsd-sessions", "name": "Sessions" + }, + { + "id": "gsd-activity", + "name": "Activity" } ] }, @@ -201,6 +234,11 @@ "command": "gsd.refreshSessions", "when": "view == gsd-sessions", "group": "navigation" + }, + { + "command": "gsd.clearActivity", + "when": "view == gsd-activity", + "group": "navigation" } ] }, @@ -235,6 +273,30 @@ "type": "boolean", "default": true, "description": "Show 'Ask GSD' code lens above functions and classes" + }, + "gsd.showProgressNotifications": { + "type": "boolean", + "default": true, + "description": "Show progress notification while the agent is working" + }, + "gsd.activityFeedMaxItems": { + "type": "number", + "default": 100, + "minimum": 10, + "maximum": 500, + "description": "Maximum number of items shown in the Activity feed" + }, + "gsd.showContextWarning": { + "type": "boolean", + "default": true, + "description": "Warn when context window usage exceeds the threshold" + }, + "gsd.contextWarningThreshold": { + "type": "number", + "default": 80, + "minimum": 50, + "maximum": 95, + "description": "Context window usage percentage that triggers a warning" } } } diff --git a/vscode-extension/src/activity-feed.ts b/vscode-extension/src/activity-feed.ts new file mode 100644 index 000000000..a07117ec9 --- /dev/null +++ b/vscode-extension/src/activity-feed.ts @@ -0,0 +1,212 @@ +import * as vscode from "vscode"; +import type { GsdClient, AgentEvent } from "./gsd-client.js"; + +interface ActivityItem { + id: number; + type: "tool" | "agent"; + label: string; + detail: string; + icon: vscode.ThemeIcon; + timestamp: number; + duration?: number; + filePath?: string; + status: "running" | "success" | "error"; +} + +const TOOL_ICONS: Record = { + Read: "file", + Write: "new-file", + Edit: "edit", + Bash: "terminal", + Grep: "search", + Glob: "file-directory", + Agent: "organization", +}; + +function toolSummary(toolName: string, toolInput: Record): { label: string; filePath?: string } { + const name = toolName ?? "Unknown"; + switch (name) { + case "Read": { + const p = String(toolInput?.file_path ?? toolInput?.path ?? ""); + const short = p.split(/[\\/]/).pop() ?? p; + return { label: `Read ${short}`, filePath: p || undefined }; + } + case "Write": { + const p = String(toolInput?.file_path ?? ""); + const short = p.split(/[\\/]/).pop() ?? p; + return { label: `Write ${short}`, filePath: p || undefined }; + } + case "Edit": { + const p = String(toolInput?.file_path ?? ""); + const short = p.split(/[\\/]/).pop() ?? p; + return { label: `Edit ${short}`, filePath: p || undefined }; + } + case "Bash": { + const cmd = String(toolInput?.command ?? "").slice(0, 60); + return { label: `Bash: ${cmd}` }; + } + case "Grep": { + const pat = String(toolInput?.pattern ?? "").slice(0, 40); + return { label: `Grep: ${pat}` }; + } + case "Glob": { + const pat = String(toolInput?.pattern ?? "").slice(0, 40); + return { label: `Glob: ${pat}` }; + } + default: + return { label: name }; + } +} + +/** + * TreeDataProvider that shows real-time tool executions from the GSD agent. + * Listens to tool_execution_start/end and agent_start/end events. + */ +export class GsdActivityFeedProvider implements vscode.TreeDataProvider, vscode.Disposable { + public static readonly viewId = "gsd-activity"; + + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + private items: ActivityItem[] = []; + private nextId = 0; + private runningTools = new Map(); // toolUseId -> item id + private maxItems: number; + private disposables: vscode.Disposable[] = []; + + constructor(private readonly client: GsdClient) { + this.maxItems = vscode.workspace.getConfiguration("gsd").get("activityFeedMaxItems", 100); + + this.disposables.push( + this._onDidChangeTreeData, + client.onEvent((evt) => this.handleEvent(evt)), + client.onConnectionChange((connected) => { + if (!connected) { + this.runningTools.clear(); + } + this._onDidChangeTreeData.fire(); + }), + vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration("gsd.activityFeedMaxItems")) { + this.maxItems = vscode.workspace.getConfiguration("gsd").get("activityFeedMaxItems", 100); + } + }), + ); + } + + getTreeItem(element: ActivityItem): vscode.TreeItem { + const item = new vscode.TreeItem(element.label, vscode.TreeItemCollapsibleState.None); + item.iconPath = element.icon; + item.description = element.duration !== undefined + ? `${element.duration}ms` + : element.status === "running" + ? "running..." + : ""; + item.tooltip = `${element.detail}\n${new Date(element.timestamp).toLocaleTimeString()}`; + + if (element.filePath) { + item.command = { + command: "vscode.open", + title: "Open File", + arguments: [vscode.Uri.file(element.filePath)], + }; + } + + return item; + } + + getChildren(): ActivityItem[] { + // Show newest first + return [...this.items].reverse(); + } + + clear(): void { + this.items = []; + this.runningTools.clear(); + this._onDidChangeTreeData.fire(); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + } + + private handleEvent(evt: AgentEvent): void { + switch (evt.type) { + case "agent_start": { + this.addItem({ + type: "agent", + label: "Agent started", + detail: "Agent began processing", + icon: new vscode.ThemeIcon("play", new vscode.ThemeColor("testing.iconPassed")), + status: "running", + }); + break; + } + case "agent_end": { + this.addItem({ + type: "agent", + label: "Agent finished", + detail: "Agent completed processing", + icon: new vscode.ThemeIcon("check", new vscode.ThemeColor("testing.iconPassed")), + status: "success", + }); + break; + } + case "tool_execution_start": { + const toolName = String(evt.toolName ?? ""); + const toolInput = (evt.toolInput ?? {}) as Record; + const toolUseId = String(evt.toolUseId ?? ""); + const { label, filePath } = toolSummary(toolName, toolInput); + const iconName = TOOL_ICONS[toolName] ?? "tools"; + + const id = this.addItem({ + type: "tool", + label, + detail: `Tool: ${toolName}`, + icon: new vscode.ThemeIcon(iconName, new vscode.ThemeColor("charts.yellow")), + status: "running", + filePath, + }); + + if (toolUseId) { + this.runningTools.set(toolUseId, id); + } + break; + } + case "tool_execution_end": { + const toolUseId = String(evt.toolUseId ?? ""); + const itemId = this.runningTools.get(toolUseId); + if (itemId !== undefined) { + this.runningTools.delete(toolUseId); + const item = this.items.find((i) => i.id === itemId); + if (item) { + const isError = evt.error === true || evt.isError === true; + item.status = isError ? "error" : "success"; + item.duration = Date.now() - item.timestamp; + item.icon = new vscode.ThemeIcon( + isError ? "error" : "check", + new vscode.ThemeColor(isError ? "testing.iconFailed" : "testing.iconPassed"), + ); + this._onDidChangeTreeData.fire(); + } + } + break; + } + } + } + + private addItem(partial: Omit): number { + const id = this.nextId++; + this.items.push({ ...partial, id, timestamp: Date.now() }); + + // Evict old items + while (this.items.length > this.maxItems) { + this.items.shift(); + } + + this._onDidChangeTreeData.fire(); + return id; + } +} diff --git a/vscode-extension/src/code-lens.ts b/vscode-extension/src/code-lens.ts index 7fe40ced9..eb6754ad7 100644 --- a/vscode-extension/src/code-lens.ts +++ b/vscode-extension/src/code-lens.ts @@ -96,13 +96,32 @@ export class GsdCodeLensProvider implements vscode.CodeLensProvider, vscode.Disp seen.add(i); const symbolName = match[1]; const range = new vscode.Range(i, 0, i, text.length); + const args = [symbolName, fileName, i + 1]; lenses.push( new vscode.CodeLens(range, { title: "$(hubot) Ask GSD", tooltip: `Ask GSD to explain ${symbolName}`, command: "gsd.askAboutSymbol", - arguments: [symbolName, fileName, i + 1], + arguments: args, + }), + new vscode.CodeLens(range, { + title: "$(pencil) Refactor", + tooltip: `Refactor ${symbolName}`, + command: "gsd.refactorSymbol", + arguments: args, + }), + new vscode.CodeLens(range, { + title: "$(bug) Find Bugs", + tooltip: `Review ${symbolName} for bugs`, + command: "gsd.findBugsSymbol", + arguments: args, + }), + new vscode.CodeLens(range, { + title: "$(beaker) Tests", + tooltip: `Generate tests for ${symbolName}`, + command: "gsd.generateTestsSymbol", + arguments: args, }), ); } diff --git a/vscode-extension/src/conversation-history.ts b/vscode-extension/src/conversation-history.ts index bebde2190..a8904cc63 100644 --- a/vscode-extension/src/conversation-history.ts +++ b/vscode-extension/src/conversation-history.ts @@ -4,6 +4,9 @@ import type { GsdClient } from "./gsd-client.js"; interface ContentBlock { type: string; text?: string; + name?: string; + input?: Record; + content?: string | ContentBlock[]; [key: string]: unknown; } @@ -14,7 +17,8 @@ interface ConversationMessage { /** * Webview panel that displays the full conversation history for the - * current GSD session using the get_messages RPC call. + * current GSD session using the get_messages RPC call. Shows tool calls, + * thinking blocks, search/filter, and fork-from-here actions. */ export class GsdConversationHistoryPanel implements vscode.Disposable { private static currentPanel: GsdConversationHistoryPanel | undefined; @@ -65,9 +69,19 @@ export class GsdConversationHistoryPanel implements vscode.Disposable { this.panel.onDidDispose(() => this.dispose(), null, this.disposables); this.panel.webview.onDidReceiveMessage( - async (msg: { command: string }) => { + async (msg: { command: string; entryId?: string }) => { if (msg.command === "refresh") { await this.refresh(); + } else if (msg.command === "fork" && msg.entryId) { + try { + const result = await this.client.forkSession(msg.entryId); + if (!result.cancelled) { + vscode.window.showInformationMessage("Session forked successfully."); + } + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Fork failed: ${errMsg}`); + } } }, null, @@ -100,16 +114,23 @@ export class GsdConversationHistoryPanel implements vscode.Disposable { private getHtml(messages: ConversationMessage[], errorMessage?: string): string { const nonce = getNonce(); + const visibleMessages = messages.filter((m) => m.role === "user" || m.role === "assistant"); - const renderedMessages = messages - .filter((m) => m.role === "user" || m.role === "assistant") - .map((msg) => { - const text = extractText(msg.content); - if (!text.trim()) return ""; + const renderedMessages = visibleMessages + .map((msg, idx) => { const isUser = msg.role === "user"; - return `
-
${isUser ? "You" : "GSD"}
-
${escapeHtml(text)}
+ const blocks = renderContentBlocks(msg.content); + if (!blocks.trim()) return ""; + + const entryId = `msg-${idx}`; + const forkBtn = ``; + + return `
+
+ ${isUser ? "You" : "GSD"} + ${forkBtn} +
+
${blocks}
`; }) .filter(Boolean) @@ -140,6 +161,15 @@ export class GsdConversationHistoryPanel implements vscode.Disposable { gap: 8px; margin-bottom: 16px; } + .search-input { + flex: 1; + padding: 5px 10px; + border: 1px solid var(--vscode-input-border); + background: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border-radius: 2px; + font-size: var(--vscode-font-size); + } .btn { padding: 5px 12px; border: none; @@ -148,11 +178,13 @@ export class GsdConversationHistoryPanel implements vscode.Disposable { font-size: var(--vscode-font-size); color: var(--vscode-button-foreground); background: var(--vscode-button-background); + white-space: nowrap; } .btn:hover { background: var(--vscode-button-hoverBackground); } .count { font-size: 12px; opacity: 0.6; + white-space: nowrap; } .error { color: var(--vscode-errorForeground); @@ -171,59 +203,211 @@ export class GsdConversationHistoryPanel implements vscode.Disposable { overflow: hidden; border: 1px solid var(--vscode-panel-border); } + .message.hidden { + display: none; + } + .role-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 3px 10px; + background: var(--vscode-panel-border); + } + .message.assistant .role-row { + background: var(--vscode-focusBorder); + } .role { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; - padding: 3px 10px; - background: var(--vscode-panel-border); opacity: 0.85; } .message.assistant .role { - background: var(--vscode-focusBorder); color: var(--vscode-button-foreground); opacity: 1; } + .fork-btn { + padding: 1px 6px; + font-size: 10px; + border: 1px solid var(--vscode-foreground); + background: transparent; + color: var(--vscode-foreground); + border-radius: 3px; + cursor: pointer; + opacity: 0; + transition: opacity 0.15s; + } + .message:hover .fork-btn { + opacity: 0.6; + } + .fork-btn:hover { + opacity: 1 !important; + background: var(--vscode-button-secondaryBackground); + } .content { padding: 10px 12px; white-space: pre-wrap; word-break: break-word; line-height: 1.55; } + .tool-block { + margin: 8px 0; + padding: 6px 10px; + background: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + font-size: 12px; + } + .tool-header { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + user-select: none; + font-weight: 600; + opacity: 0.8; + } + .tool-header:hover { + opacity: 1; + } + .tool-body { + display: none; + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid var(--vscode-panel-border); + white-space: pre-wrap; + word-break: break-all; + max-height: 200px; + overflow-y: auto; + opacity: 0.75; + } + .tool-block.expanded .tool-body { + display: block; + } + .thinking-block { + margin: 8px 0; + padding: 6px 10px; + background: var(--vscode-editor-background); + border-left: 3px solid var(--vscode-focusBorder); + border-radius: 2px; + font-size: 12px; + opacity: 0.65; + font-style: italic; + } + .thinking-header { + cursor: pointer; + user-select: none; + font-weight: 600; + } + .thinking-body { + display: none; + margin-top: 4px; + white-space: pre-wrap; + max-height: 300px; + overflow-y: auto; + } + .thinking-block.expanded .thinking-body { + display: block; + } + code { + background: var(--vscode-editor-background); + padding: 1px 4px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 0.92em; + }

Conversation History

+ - ${messages.length > 0 ? `${messages.length} message${messages.length === 1 ? "" : "s"}` : ""} + ${visibleMessages.length > 0 ? `${visibleMessages.length} message${visibleMessages.length === 1 ? "" : "s"}` : ""}
${errorMessage ? `
${escapeHtml(errorMessage)}
` : ""} - ${!errorMessage && renderedMessages === "" ? '
No messages in this session.
' : renderedMessages} +
+ ${!errorMessage && renderedMessages === "" ? '
No messages in this session.
' : renderedMessages} +
`; } } -function extractText(content: string | ContentBlock[]): string { - if (typeof content === "string") return content; - if (Array.isArray(content)) { - return content - .map((block) => { - if (typeof block === "string") return block; - if (block?.type === "text" && typeof block.text === "string") return block.text; - return ""; - }) - .join(""); - } - return ""; +function renderContentBlocks(content: string | ContentBlock[]): string { + if (typeof content === "string") return escapeHtml(content); + if (!Array.isArray(content)) return ""; + + return content + .map((block) => { + if (typeof block === "string") return escapeHtml(block); + + switch (block.type) { + case "text": + return escapeHtml(block.text ?? ""); + + case "thinking": + if (!block.text) return ""; + return `
+
Thinking...
+
${escapeHtml(block.text)}
+
`; + + case "tool_use": + return `
+
Tool: ${escapeHtml(block.name ?? "unknown")}
+
${escapeHtml(JSON.stringify(block.input ?? {}, null, 2))}
+
`; + + case "tool_result": { + const resultText = typeof block.content === "string" + ? block.content + : Array.isArray(block.content) + ? block.content.map((b) => (typeof b === "string" ? b : b?.text ?? "")).join("") + : ""; + if (!resultText) return ""; + const truncated = resultText.length > 500 ? resultText.slice(0, 500) + "..." : resultText; + return `
+
Tool Result
+
${escapeHtml(truncated)}
+
`; + } + + default: + return ""; + } + }) + .join(""); } function escapeHtml(text: string): string { diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index f125cebd9..d909c4e12 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -8,11 +8,13 @@ import { GsdSessionTreeProvider } from "./session-tree.js"; import { GsdConversationHistoryPanel } from "./conversation-history.js"; import { GsdSlashCompletionProvider } from "./slash-completion.js"; import { GsdCodeLensProvider } from "./code-lens.js"; +import { GsdActivityFeedProvider } from "./activity-feed.js"; let client: GsdClient | undefined; let sidebarProvider: GsdSidebarProvider | undefined; let fileDecorations: GsdFileDecorationProvider | undefined; let sessionTreeProvider: GsdSessionTreeProvider | undefined; +let activityFeedProvider: GsdActivityFeedProvider | undefined; function requireConnected(): boolean { if (!client?.isConnected) { @@ -118,6 +120,98 @@ export function activate(context: vscode.ExtensionContext): void { vscode.window.registerTreeDataProvider(GsdSessionTreeProvider.viewId, sessionTreeProvider), ); + // -- Activity feed ----------------------------------------------------- + + activityFeedProvider = new GsdActivityFeedProvider(client); + context.subscriptions.push( + activityFeedProvider, + vscode.window.registerTreeDataProvider(GsdActivityFeedProvider.viewId, activityFeedProvider), + ); + + // -- Progress notifications -------------------------------------------- + + let currentProgress: { resolve: () => void } | undefined; + + client.onEvent((evt) => { + const showProgress = vscode.workspace.getConfiguration("gsd").get("showProgressNotifications", true); + if (!showProgress) return; + + if (evt.type === "agent_start" && !currentProgress) { + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "GSD Agent", + cancellable: true, + }, + (progress, token) => { + token.onCancellationRequested(() => { + client?.abort().catch(() => {}); + }); + + // Listen for tool events to update progress message + const toolListener = client!.onEvent((toolEvt) => { + if (toolEvt.type === "tool_execution_start") { + const toolName = String(toolEvt.toolName ?? ""); + progress.report({ message: `Running ${toolName}...` }); + } + }); + + return new Promise((resolve) => { + currentProgress = { resolve }; + // Also clean up if disposed + token.onCancellationRequested(() => { + toolListener.dispose(); + currentProgress = undefined; + resolve(); + }); + }).finally(() => { + toolListener.dispose(); + }); + }, + ); + } else if (evt.type === "agent_end" && currentProgress) { + currentProgress.resolve(); + currentProgress = undefined; + } + }); + + // -- Context window warning -------------------------------------------- + + let lastContextWarning = 0; + client.onEvent(async (evt) => { + if (evt.type !== "message_end") return; + const showWarning = vscode.workspace.getConfiguration("gsd").get("showContextWarning", true); + if (!showWarning) return; + + // Throttle: at most once per 60 seconds + if (Date.now() - lastContextWarning < 60_000) return; + + try { + const [state, stats] = await Promise.all([ + client!.getState().catch(() => null), + client!.getSessionStats().catch(() => null), + ]); + const contextWindow = state?.model?.contextWindow ?? 0; + const totalTokens = (stats?.inputTokens ?? 0) + (stats?.outputTokens ?? 0); + if (contextWindow <= 0) return; + + const threshold = vscode.workspace.getConfiguration("gsd").get("contextWarningThreshold", 80); + const pct = Math.round((totalTokens / contextWindow) * 100); + if (pct >= threshold) { + lastContextWarning = Date.now(); + const action = await vscode.window.showWarningMessage( + `Context window ${pct}% full (${Math.round(totalTokens / 1000)}k / ${Math.round(contextWindow / 1000)}k). Consider compacting.`, + "Compact Now", + ); + if (action === "Compact Now") { + await vscode.commands.executeCommand("gsd.compact"); + } + } + } catch { + // ignore + } + }); + // -- Chat participant --------------------------------------------------- context.subscriptions.push(registerChatParticipant(context, client)); @@ -515,6 +609,121 @@ export function activate(context: vscode.ExtensionContext): void { }), ); + // Clear Activity Feed + context.subscriptions.push( + vscode.commands.registerCommand("gsd.clearActivity", () => { + activityFeedProvider?.clear(); + }), + ); + + // Fork Session + context.subscriptions.push( + vscode.commands.registerCommand("gsd.forkSession", async () => { + if (!requireConnected()) return; + try { + const messages = await client!.getForkMessages(); + if (messages.length === 0) { + vscode.window.showInformationMessage("No fork points available."); + return; + } + const items = messages.map((m) => ({ + label: m.text.slice(0, 80) + (m.text.length > 80 ? "..." : ""), + description: m.entryId, + entryId: m.entryId, + })); + const selected = await vscode.window.showQuickPick(items, { + placeHolder: "Select a message to fork from", + }); + if (!selected) return; + const result = await client!.forkSession(selected.entryId); + if (!result.cancelled) { + vscode.window.showInformationMessage("Session forked successfully."); + sidebarProvider?.refresh(); + sessionTreeProvider?.refresh(); + } + } catch (err) { + handleError(err, "Failed to fork session"); + } + }), + ); + + // Toggle Steering Mode + context.subscriptions.push( + vscode.commands.registerCommand("gsd.toggleSteeringMode", async () => { + if (!requireConnected()) return; + try { + const state = await client!.getState(); + const next = state.steeringMode === "all" ? "one-at-a-time" : "all"; + await client!.setSteeringMode(next); + vscode.window.showInformationMessage(`Steering mode: ${next}`); + sidebarProvider?.refresh(); + } catch (err) { + handleError(err, "Failed to toggle steering mode"); + } + }), + ); + + // Toggle Follow-Up Mode + context.subscriptions.push( + vscode.commands.registerCommand("gsd.toggleFollowUpMode", async () => { + if (!requireConnected()) return; + try { + const state = await client!.getState(); + const next = state.followUpMode === "all" ? "one-at-a-time" : "all"; + await client!.setFollowUpMode(next); + vscode.window.showInformationMessage(`Follow-up mode: ${next}`); + sidebarProvider?.refresh(); + } catch (err) { + handleError(err, "Failed to toggle follow-up mode"); + } + }), + ); + + // Refactor Symbol (code lens) + context.subscriptions.push( + vscode.commands.registerCommand( + "gsd.refactorSymbol", + async (symbolName: string, fileName: string, lineNumber: number) => { + if (!requireConnected()) return; + try { + await client!.sendPrompt(`Refactor the \`${symbolName}\` function/class in ${fileName} (line ${lineNumber}). Improve clarity, performance, or structure while preserving behavior.`); + } catch (err) { + handleError(err, "Failed to send refactor request"); + } + }, + ), + ); + + // Find Bugs in Symbol (code lens) + context.subscriptions.push( + vscode.commands.registerCommand( + "gsd.findBugsSymbol", + async (symbolName: string, fileName: string, lineNumber: number) => { + if (!requireConnected()) return; + try { + await client!.sendPrompt(`Review the \`${symbolName}\` function/class in ${fileName} (line ${lineNumber}) for potential bugs, edge cases, and issues.`); + } catch (err) { + handleError(err, "Failed to send bug review request"); + } + }, + ), + ); + + // Generate Tests for Symbol (code lens) + context.subscriptions.push( + vscode.commands.registerCommand( + "gsd.generateTestsSymbol", + async (symbolName: string, fileName: string, lineNumber: number) => { + if (!requireConnected()) return; + try { + await client!.sendPrompt(`Generate comprehensive tests for the \`${symbolName}\` function/class in ${fileName} (line ${lineNumber}). Cover success paths, edge cases, and error scenarios.`); + } catch (err) { + handleError(err, "Failed to send test generation request"); + } + }, + ), + ); + // Toggle Auto-Retry context.subscriptions.push( vscode.commands.registerCommand("gsd.toggleAutoRetry", async () => { @@ -592,8 +801,10 @@ export function deactivate(): void { sidebarProvider?.dispose(); fileDecorations?.dispose(); sessionTreeProvider?.dispose(); + activityFeedProvider?.dispose(); client = undefined; sidebarProvider = undefined; fileDecorations = undefined; sessionTreeProvider = undefined; + activityFeedProvider = undefined; } diff --git a/vscode-extension/src/gsd-client.ts b/vscode-extension/src/gsd-client.ts index 2e37befa2..61008b90a 100644 --- a/vscode-extension/src/gsd-client.ts +++ b/vscode-extension/src/gsd-client.ts @@ -492,6 +492,48 @@ export class GsdClient implements vscode.Disposable { return (response.data as { commands: SlashCommand[] }).commands; } + // ========================================================================= + // Fork + // ========================================================================= + + /** + * Get messages that can be used as fork points. + */ + async getForkMessages(): Promise<{ entryId: string; text: string }[]> { + const response = await this.send({ type: "get_fork_messages" }); + this.assertSuccess(response); + return (response.data as { messages: { entryId: string; text: string }[] }).messages; + } + + /** + * Fork the session at the given entry point. + */ + async forkSession(entryId: string): Promise<{ text: string; cancelled: boolean }> { + const response = await this.send({ type: "fork", entryId }); + this.assertSuccess(response); + return response.data as { text: string; cancelled: boolean }; + } + + // ========================================================================= + // Queue Modes + // ========================================================================= + + /** + * Set steering queue mode. + */ + async setSteeringMode(mode: "all" | "one-at-a-time"): Promise { + const response = await this.send({ type: "set_steering_mode", mode }); + this.assertSuccess(response); + } + + /** + * Set follow-up queue mode. + */ + async setFollowUpMode(mode: "all" | "one-at-a-time"): Promise { + const response = await this.send({ type: "set_follow_up_mode", mode }); + this.assertSuccess(response); + } + dispose(): void { this.stop(); for (const d of this.disposables) { diff --git a/vscode-extension/src/sidebar.ts b/vscode-extension/src/sidebar.ts index f8a8e55ec..12c718633 100644 --- a/vscode-extension/src/sidebar.ts +++ b/vscode-extension/src/sidebar.ts @@ -105,6 +105,50 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { case "copyLastResponse": await vscode.commands.executeCommand("gsd.copyLastResponse"); break; + case "autoMode": + if (this.client.isConnected) { + await this.client.sendPrompt("/gsd auto").catch(() => {}); + } + break; + case "nextUnit": + if (this.client.isConnected) { + await this.client.sendPrompt("/gsd next").catch(() => {}); + } + break; + case "quickTask": { + const quickInput = await vscode.window.showInputBox({ + prompt: "Describe the quick task", + placeHolder: "e.g. fix the typo in README", + }); + if (quickInput && this.client.isConnected) { + await this.client.sendPrompt(`/gsd quick ${quickInput}`).catch(() => {}); + } + break; + } + case "capture": { + const thought = await vscode.window.showInputBox({ + prompt: "Capture a thought", + placeHolder: "e.g. we should also handle the edge case for...", + }); + if (thought && this.client.isConnected) { + await this.client.sendPrompt(`/gsd capture ${thought}`).catch(() => {}); + } + break; + } + case "status": + if (this.client.isConnected) { + await this.client.sendPrompt("/gsd status").catch(() => {}); + } + break; + case "forkSession": + await vscode.commands.executeCommand("gsd.forkSession"); + break; + case "toggleSteeringMode": + await vscode.commands.executeCommand("gsd.toggleSteeringMode"); + break; + case "toggleFollowUpMode": + await vscode.commands.executeCommand("gsd.toggleFollowUpMode"); + break; } }); @@ -134,6 +178,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { let autoCompaction = false; let autoRetry = false; let stats: SessionStats | null = null; + let contextWindow = 0; + let steeringMode: "all" | "one-at-a-time" = "all"; + let followUpMode: "all" | "one-at-a-time" = "all"; if (this.client.isConnected) { autoRetry = this.client.autoRetryEnabled; @@ -150,6 +197,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { isStreaming = state.isStreaming; isCompacting = state.isCompacting; autoCompaction = state.autoCompactionEnabled; + contextWindow = state.model?.contextWindow ?? 0; + steeringMode = state.steeringMode; + followUpMode = state.followUpMode; } catch { // State fetch failed, show defaults } @@ -176,6 +226,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { autoCompaction, autoRetry, stats, + contextWindow, + steeringMode, + followUpMode, }); } @@ -201,6 +254,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { autoCompaction: boolean; autoRetry: boolean; stats: SessionStats | null; + contextWindow: number; + steeringMode: "all" | "one-at-a-time"; + followUpMode: "all" | "one-at-a-time"; }): string { const statusColor = info.connected ? "#4ec9b0" : "#f44747"; const statusText = info.connected @@ -237,6 +293,21 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { ? `
Agent is working...
` : ""; + // Context window usage + const totalTokens = (info.stats?.inputTokens ?? 0) + (info.stats?.outputTokens ?? 0); + const contextPct = info.contextWindow > 0 ? Math.min(100, Math.round((totalTokens / info.contextWindow) * 100)) : 0; + const contextColor = contextPct > 80 ? "#f44747" : contextPct > 50 ? "#cca700" : "#4ec9b0"; + const contextLabel = info.contextWindow > 0 + ? `${contextPct}% (${Math.round(totalTokens / 1000)}k / ${Math.round(info.contextWindow / 1000)}k)` + : "N/A"; + + const steeringBadge = info.steeringMode === "one-at-a-time" + ? `1-at-a-time` + : `all`; + const followUpBadge = info.followUpMode === "one-at-a-time" + ? `1-at-a-time` + : `all`; + const nonce = getNonce(); return /* html */ ` @@ -376,6 +447,23 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { text-align: right; font-variant-numeric: tabular-nums; } + .context-bar-outer { + width: 100%; + height: 6px; + background: var(--vscode-editor-background); + border-radius: 3px; + overflow: hidden; + margin: 4px 0 2px; + } + .context-bar-inner { + height: 100%; + border-radius: 3px; + transition: width 0.3s ease; + } + .context-label { + font-size: 11px; + opacity: 0.7; + } @@ -410,6 +498,14 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { Auto-retry ${autoRetryBadge} + + Steering + ${info.steeringMode === "one-at-a-time" ? "1-at-a-time" : "all"} + + + Follow-up + ${info.followUpMode === "one-at-a-time" ? "1-at-a-time" : "all"} +
@@ -433,6 +529,36 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider { ${cost} + + ${info.contextWindow > 0 ? ` +
+
Context Window
+
+
+
+
${contextLabel}
+
+ ` : ""} + ` : ""} + + ${info.connected ? ` +
+
Workflow
+
+
+ + +
+
+ + +
+
+ + +
+
+
` : ""}