feat(vscode): sidebar redesign, SCM provider, checkpoints, diagnostics [3/3]
Comprehensive vscode extension redesign with sidebar reorganization, new features, and enhanced agent integration: - Redesign sidebar UI: reduce 6 panels to 3, declutter layout - SCM provider for tracking agent-modified files - Checkpoint system for saving/restoring agent state - Diagnostic integration for surfacing errors in editor - Line-level editor decorations for agent-modified lines - Git integration for visualizing agent changes - Execution plan viewer for live agent step visualization - Approval/permissions mode system - Auto-inject editor selection and diagnostics in chat - Route workflow buttons through Chat panel - Handle extension UI requests from agent (select, confirm, input) - Session persistence, ISO timestamp support, descriptive checkpoints - Bump to v0.3.0
This commit is contained in:
parent
0a2c9b64c6
commit
59d80e200a
18 changed files with 2343 additions and 389 deletions
|
|
@ -1,24 +1,45 @@
|
|||
# Changelog
|
||||
|
||||
## [0.3.0]
|
||||
|
||||
### Added
|
||||
|
||||
- **SCM provider** — "GSD Agent" appears in Source Control panel with accept/discard per-file diffs
|
||||
- **Change tracker** — captures original file content before agent modifications for diff and rollback
|
||||
- **Checkpoints** — automatic snapshots on each agent turn with restore capability
|
||||
- **Diagnostic bridge** — "Fix Problems in File" and "Fix All Problems" commands read VS Code diagnostics and send to agent
|
||||
- **Line-level decorations** — green/yellow highlights on agent-modified lines with gutter indicators
|
||||
- **Chat context injection** — auto-includes editor selection and file diagnostics when relevant
|
||||
- **Git integration** — commit agent changes, create branches, show diffs
|
||||
- **Approval modes** — auto-approve, ask (prompts before writes), plan-only (read-only)
|
||||
- **UI request handling** — agent questions, confirmations, and selections now show as VS Code dialogs instead of hanging
|
||||
- **Fix Errors button** — quick access to diagnostic fixing in sidebar Actions
|
||||
- **5 new settings** — `showProgressNotifications`, `activityFeedMaxItems`, `showContextWarning`, `contextWarningThreshold`, `approvalMode`
|
||||
|
||||
### Changed
|
||||
|
||||
- **Sidebar redesign** — compact card-based layout with collapsible sections, pill toggles, hidden empty data
|
||||
- **Workflow buttons** now route through Chat panel so responses are visible
|
||||
- **Slash completion** filtered to `/gsd` commands only
|
||||
- **Checkpoint labels** show timestamp + first action (e.g., "10:32 — Edit sidebar.ts")
|
||||
- **Session tree** supports ISO timestamp filenames (GSD's actual format)
|
||||
- **Session persistence** enabled (removed `--no-session` flag)
|
||||
- **Progress notifications** disabled by default (Chat panel provides inline progress)
|
||||
- **Sidebar reduced** from 6 panels to 3 (GSD Agent, Sessions, Activity)
|
||||
- **Settings section** starts collapsed by default
|
||||
|
||||
## [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
|
||||
- **Activity feed** — real-time TreeView showing tool executions with status icons, duration, and click-to-open
|
||||
- **Workflow controls** — sidebar buttons for Auto, Next, Quick Task, Capture
|
||||
- **Context window indicator** — color-coded usage bar in sidebar with threshold warnings
|
||||
- **Session forking** — fork from any message via QuickPick
|
||||
- **Queue mode controls** — toggle steering and follow-up modes from the sidebar
|
||||
- **Enhanced conversation history** — tool call rendering, collapsible thinking blocks, search/filter, fork-from-here
|
||||
- **Enhanced code lens** — Refactor, Find Bugs, and Generate Tests alongside Ask GSD
|
||||
- **8 new commands** (33 total)
|
||||
|
||||
## [0.1.0]
|
||||
|
||||
|
|
@ -31,7 +52,7 @@ Initial release.
|
|||
- 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
|
||||
- Slash command completion — auto-complete for `/gsd` commands
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -1,88 +1,193 @@
|
|||
# GSD-2 — VS Code Extension
|
||||
|
||||
Control the [GSD-2 coding agent](https://github.com/gsd-build/gsd-2) directly from VS Code. Run autonomous coding sessions, chat with `@gsd` in VS Code Chat, and monitor your agent from a sidebar dashboard — all without leaving the editor.
|
||||
Control the [GSD-2 coding agent](https://github.com/gsd-build/gsd-2) directly from VS Code. Run autonomous coding sessions, chat with `@gsd`, monitor agent activity in real-time, review and accept/reject changes, and manage your workflow — all without leaving the editor.
|
||||
|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
GSD must be installed before activating this extension:
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
Node.js ≥ 22.0.0 and Git are required.
|
||||
|
||||
## Features
|
||||
|
||||
### Sidebar Dashboard
|
||||
|
||||
Click the GSD icon in the Activity Bar to open the agent dashboard. It shows:
|
||||
|
||||
- Connection status (connected / disconnected)
|
||||
- Active model and provider
|
||||
- Thinking level
|
||||
- Token usage and session cost
|
||||
- Quick action buttons: Start, Stop, New Session, Compact, Abort
|
||||
|
||||
### Chat Integration (`@gsd`)
|
||||
|
||||
Use `@gsd` in VS Code Chat (`Ctrl+Shift+I`) to send messages to the agent:
|
||||
|
||||
```
|
||||
@gsd refactor the auth module to use JWT
|
||||
@gsd /gsd auto
|
||||
@gsd what's the current milestone status?
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
All commands are accessible via `Ctrl+Shift+P`:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| **GSD: Start Agent** | Connect to the GSD agent |
|
||||
| **GSD: Stop Agent** | Disconnect the agent |
|
||||
| **GSD: New Session** | Start a fresh conversation |
|
||||
| **GSD: Send Message** | Send a message to the agent |
|
||||
| **GSD: Abort Current Operation** | Interrupt the current operation |
|
||||
| **GSD: Steer Agent** | Send a steering message mid-operation |
|
||||
| **GSD: Switch Model** | Pick a model from QuickPick |
|
||||
| **GSD: Cycle Model** | Rotate to the next configured model |
|
||||
| **GSD: Set Thinking Level** | Choose off / low / medium / high |
|
||||
| **GSD: Cycle Thinking Level** | Rotate through thinking levels |
|
||||
| **GSD: Compact Context** | Manually trigger context compaction |
|
||||
| **GSD: Export Conversation as HTML** | Save the session as HTML |
|
||||
| **GSD: Show Session Stats** | Display token usage and cost |
|
||||
| **GSD: Run Bash Command** | Execute a shell command via the agent |
|
||||
| **GSD: List Available Commands** | Browse and run GSD slash commands |
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Command |
|
||||
|----------|---------|
|
||||
| `Ctrl+Shift+G Ctrl+Shift+N` | New Session |
|
||||
| `Ctrl+Shift+G Ctrl+Shift+M` | Cycle Model |
|
||||
| `Ctrl+Shift+G Ctrl+Shift+T` | Cycle Thinking Level |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `gsd.binaryPath` | `"gsd"` | Path to the GSD binary if not on PATH |
|
||||
| `gsd.autoStart` | `false` | Start the agent automatically when the extension activates |
|
||||
| `gsd.autoCompaction` | `true` | Enable automatic context compaction |
|
||||
- **GSD-2** installed globally: `npm install -g gsd-pi`
|
||||
- **Node.js** >= 22.0.0
|
||||
- **Git** installed and on PATH
|
||||
- **VS Code** >= 1.95.0
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Install GSD: `npm install -g gsd-pi`
|
||||
2. Install this extension
|
||||
3. Open a project folder in VS Code
|
||||
4. `Ctrl+Shift+P` → **GSD: Start Agent**
|
||||
5. Use `@gsd` in Chat or the sidebar to interact with the agent
|
||||
4. Click the **GSD icon** in the Activity Bar (left sidebar)
|
||||
5. Click **Start Agent** or run `Ctrl+Shift+P` > **GSD: Start Agent**
|
||||
6. Start chatting with `@gsd` in Chat or click **Auto** in the sidebar
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Sidebar Dashboard
|
||||
|
||||
Click the **GSD icon** in the Activity Bar. The compact header shows connection status, model, session, message count, thinking level, context usage bar, and cost — all in two lines. Sections (Workflow, Stats, Actions, Settings) are collapsible and remember their state.
|
||||
|
||||
### Workflow Controls
|
||||
|
||||
One-click buttons for GSD's core commands. All route through the Chat panel so you see the full response:
|
||||
|
||||
| Button | What it does |
|
||||
|--------|-------------|
|
||||
| **Auto** | Start autonomous mode — research, plan, execute |
|
||||
| **Next** | Execute one unit of work, then pause |
|
||||
| **Quick** | Quick task without planning (opens input) |
|
||||
| **Capture** | Capture a thought for later triage |
|
||||
|
||||
### Chat Integration (`@gsd`)
|
||||
|
||||
Use `@gsd` in VS Code Chat (`Cmd+Shift+I`) to talk to the agent:
|
||||
|
||||
```
|
||||
@gsd refactor the auth module to use JWT
|
||||
@gsd /gsd auto
|
||||
@gsd fix the errors in this file
|
||||
```
|
||||
|
||||
- **Auto-starts** the agent if not running
|
||||
- **File context** via `#file` references
|
||||
- **Selection context** — automatically includes selected code
|
||||
- **Diagnostic context** — auto-includes errors/warnings when you mention "fix" or "error"
|
||||
- **Streaming** progress, file anchors, token usage footer
|
||||
|
||||
### Source Control Integration
|
||||
|
||||
Agent-modified files appear in a dedicated **"GSD Agent"** section of the Source Control panel:
|
||||
|
||||
- **Click any file** to see a before/after diff in VS Code's native diff editor
|
||||
- **Accept** or **Discard** changes per-file via inline buttons
|
||||
- **Accept All** / **Discard All** via the SCM title bar
|
||||
- Gutter diff indicators (green/red bars) show exactly what changed
|
||||
|
||||
### Line-Level Decorations
|
||||
|
||||
When the agent modifies a file, you'll see:
|
||||
- **Green background** on newly added lines
|
||||
- **Yellow background** on modified lines
|
||||
- **Left border gutter indicator** on all agent-touched lines
|
||||
- **Hover** any decorated line to see "Modified by GSD Agent"
|
||||
|
||||
### Checkpoints & Rollback
|
||||
|
||||
Automatic checkpoints are created at the start of each agent turn. Use **Discard All** in the SCM panel to revert all agent changes to their original state, or discard individual files.
|
||||
|
||||
### Activity Feed
|
||||
|
||||
The **Activity** panel shows a real-time log of every tool the agent executes — Read, Write, Edit, Bash, Grep, Glob — with status icons (running/success/error), duration, and click-to-open for file operations.
|
||||
|
||||
### Sessions
|
||||
|
||||
The **Sessions** panel lists all past sessions for the current workspace. Click any session to switch to it. The current session is highlighted green. Sessions persist to disk automatically.
|
||||
|
||||
### Diagnostic Integration
|
||||
|
||||
- **Fix Errors** button in the sidebar reads the active file's diagnostics from the Problems panel and sends them to the agent
|
||||
- **Fix All Problems** (`Cmd+Shift+P` > GSD: Fix All Problems) collects errors/warnings across the workspace
|
||||
- Works automatically in chat — mention "fix" or "error" and diagnostics are included
|
||||
|
||||
### Code Lens
|
||||
|
||||
Four inline actions above every function and class (TS/JS/Python/Go/Rust):
|
||||
|
||||
| Action | What it does |
|
||||
|--------|-------------|
|
||||
| **Ask GSD** | Explain the function/class |
|
||||
| **Refactor** | Improve clarity, performance, or structure |
|
||||
| **Find Bugs** | Review for bugs and edge cases |
|
||||
| **Tests** | Generate test coverage |
|
||||
|
||||
### Git Integration
|
||||
|
||||
- **Commit Agent Changes** — stages and commits modified files with your message
|
||||
- **Create Branch** — create a new branch for agent work
|
||||
- **Show Diff** — view git diff of agent changes
|
||||
|
||||
### Approval Modes
|
||||
|
||||
Control how much autonomy the agent has:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| **Auto-approve** | Agent runs freely (default) |
|
||||
| **Ask** | Prompts before file writes and commands |
|
||||
| **Plan-only** | Read-only — agent can analyze but not modify |
|
||||
|
||||
Change via Settings section or `Cmd+Shift+P` > **GSD: Select Approval Mode**.
|
||||
|
||||
### Agent UI Requests
|
||||
|
||||
When the agent needs input (questions, confirmations, selections), VS Code dialogs appear automatically — no more hanging on `ask_user_questions`.
|
||||
|
||||
### Additional Features
|
||||
|
||||
- **Conversation History** — full message viewer with tool calls, thinking blocks, search, and fork-from-here
|
||||
- **Slash Command Completion** — type `/` for auto-complete of `/gsd` commands
|
||||
- **File Decorations** — "G" badge on agent-modified files in the Explorer
|
||||
- **Bash Terminal** — dedicated terminal for agent shell output
|
||||
- **Context Window Warning** — notification when context exceeds threshold
|
||||
- **Progress Notifications** — optional notification with cancel button (off by default)
|
||||
|
||||
---
|
||||
|
||||
## All Commands
|
||||
|
||||
| Command | Shortcut | Description |
|
||||
|---------|----------|-------------|
|
||||
| **GSD: Start Agent** | | Connect to the GSD agent |
|
||||
| **GSD: Stop Agent** | | Disconnect the agent |
|
||||
| **GSD: New Session** | `Cmd+Shift+G` `Cmd+Shift+N` | Start a fresh conversation |
|
||||
| **GSD: Send Message** | `Cmd+Shift+G` `Cmd+Shift+P` | Send a message to the agent |
|
||||
| **GSD: Abort** | `Cmd+Shift+G` `Cmd+Shift+A` | Interrupt the current operation |
|
||||
| **GSD: Steer Agent** | `Cmd+Shift+G` `Cmd+Shift+I` | Steering message mid-operation |
|
||||
| **GSD: Switch Model** | | Pick a model from QuickPick |
|
||||
| **GSD: Cycle Model** | `Cmd+Shift+G` `Cmd+Shift+M` | Rotate to the next model |
|
||||
| **GSD: Set Thinking Level** | | Choose off / low / medium / high |
|
||||
| **GSD: Cycle Thinking** | `Cmd+Shift+G` `Cmd+Shift+T` | Rotate through thinking levels |
|
||||
| **GSD: Compact Context** | | Trigger context compaction |
|
||||
| **GSD: Export HTML** | | Save session as HTML |
|
||||
| **GSD: Session Stats** | | Display token usage and cost |
|
||||
| **GSD: Run Bash** | | Execute a shell command |
|
||||
| **GSD: List Commands** | | Browse slash commands |
|
||||
| **GSD: Set Session Name** | | Rename current session |
|
||||
| **GSD: Copy Last Response** | | Copy to clipboard |
|
||||
| **GSD: Switch Session** | | Load a different session |
|
||||
| **GSD: Show History** | | Open conversation viewer |
|
||||
| **GSD: Fork Session** | | Fork from a previous message |
|
||||
| **GSD: Fix Problems in File** | | Send file diagnostics to agent |
|
||||
| **GSD: Fix All Problems** | | Send workspace errors to agent |
|
||||
| **GSD: Commit Agent Changes** | | Git commit modified files |
|
||||
| **GSD: Create Branch** | | Create branch for agent work |
|
||||
| **GSD: Show Agent Diff** | | View git diff |
|
||||
| **GSD: Accept All Changes** | | Accept all SCM changes |
|
||||
| **GSD: Discard All Changes** | | Revert all agent modifications |
|
||||
| **GSD: Select Approval Mode** | | Choose auto-approve/ask/plan-only |
|
||||
| **GSD: Cycle Approval Mode** | | Rotate through approval modes |
|
||||
| **GSD: Code Lens** actions | | Ask, Refactor, Find Bugs, Tests |
|
||||
|
||||
> On Windows/Linux, replace `Cmd` with `Ctrl`.
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `gsd.binaryPath` | `"gsd"` | Path to the GSD binary |
|
||||
| `gsd.autoStart` | `false` | Start agent on extension activation |
|
||||
| `gsd.autoCompaction` | `true` | Automatic context compaction |
|
||||
| `gsd.codeLens` | `true` | Code lens above functions/classes |
|
||||
| `gsd.showProgressNotifications` | `false` | Progress notification (off — Chat shows progress) |
|
||||
| `gsd.activityFeedMaxItems` | `100` | Max items in Activity feed |
|
||||
| `gsd.showContextWarning` | `true` | Warn when context exceeds threshold |
|
||||
| `gsd.contextWarningThreshold` | `80` | Context % that triggers warning |
|
||||
| `gsd.approvalMode` | `"auto-approve"` | Agent permission mode |
|
||||
|
||||
## How It Works
|
||||
|
||||
The extension spawns `gsd --mode rpc` in the background and communicates over JSON-RPC via stdin/stdout. All RPC commands are supported, including streaming events for real-time sidebar updates.
|
||||
The extension spawns `gsd --mode rpc` and communicates over JSON-RPC via stdin/stdout. Agent events stream in real-time. The change tracker captures file state before modifications for SCM diffs and rollback. UI requests from the agent (questions, confirmations) are handled via VS Code dialogs.
|
||||
|
||||
## Links
|
||||
|
||||
|
|
|
|||
BIN
vscode-extension/docs/images/overview.png
Normal file
BIN
vscode-extension/docs/images/overview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 768 KiB |
|
|
@ -3,7 +3,7 @@
|
|||
"displayName": "GSD-2",
|
||||
"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.2.0",
|
||||
"version": "0.3.0",
|
||||
"icon": "logo.jpg",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -168,6 +168,67 @@
|
|||
{
|
||||
"command": "gsd.generateTestsSymbol",
|
||||
"title": "GSD: Generate Tests for Symbol"
|
||||
},
|
||||
{
|
||||
"command": "gsd.acceptAllChanges",
|
||||
"title": "GSD: Accept All Agent Changes",
|
||||
"icon": "$(check-all)"
|
||||
},
|
||||
{
|
||||
"command": "gsd.discardAllChanges",
|
||||
"title": "GSD: Discard All Agent Changes",
|
||||
"icon": "$(discard)"
|
||||
},
|
||||
{
|
||||
"command": "gsd.acceptFileChanges",
|
||||
"title": "Accept Changes",
|
||||
"icon": "$(check)"
|
||||
},
|
||||
{
|
||||
"command": "gsd.discardFileChanges",
|
||||
"title": "Discard Changes",
|
||||
"icon": "$(discard)"
|
||||
},
|
||||
{
|
||||
"command": "gsd.restoreCheckpoint",
|
||||
"title": "GSD: Restore Checkpoint"
|
||||
},
|
||||
{
|
||||
"command": "gsd.fixProblemsInFile",
|
||||
"title": "GSD: Fix Problems in File"
|
||||
},
|
||||
{
|
||||
"command": "gsd.fixAllProblems",
|
||||
"title": "GSD: Fix All Problems"
|
||||
},
|
||||
{
|
||||
"command": "gsd.clearDiagnostics",
|
||||
"title": "GSD: Clear Agent Diagnostics"
|
||||
},
|
||||
{
|
||||
"command": "gsd.commitAgentChanges",
|
||||
"title": "GSD: Commit Agent Changes"
|
||||
},
|
||||
{
|
||||
"command": "gsd.createAgentBranch",
|
||||
"title": "GSD: Create Branch for Agent Work"
|
||||
},
|
||||
{
|
||||
"command": "gsd.showAgentDiff",
|
||||
"title": "GSD: Show Agent Diff"
|
||||
},
|
||||
{
|
||||
"command": "gsd.clearPlan",
|
||||
"title": "GSD: Clear Plan View",
|
||||
"icon": "$(clear-all)"
|
||||
},
|
||||
{
|
||||
"command": "gsd.cycleApprovalMode",
|
||||
"title": "GSD: Cycle Approval Mode"
|
||||
},
|
||||
{
|
||||
"command": "gsd.selectApprovalMode",
|
||||
"title": "GSD: Select Approval Mode"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
|
|
@ -240,6 +301,30 @@
|
|||
"when": "view == gsd-activity",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"scm/title": [
|
||||
{
|
||||
"command": "gsd.acceptAllChanges",
|
||||
"group": "navigation",
|
||||
"when": "scmProvider == gsd"
|
||||
},
|
||||
{
|
||||
"command": "gsd.discardAllChanges",
|
||||
"group": "navigation",
|
||||
"when": "scmProvider == gsd"
|
||||
}
|
||||
],
|
||||
"scm/resourceState/context": [
|
||||
{
|
||||
"command": "gsd.acceptFileChanges",
|
||||
"group": "inline",
|
||||
"when": "scmProvider == gsd"
|
||||
},
|
||||
{
|
||||
"command": "gsd.discardFileChanges",
|
||||
"group": "inline",
|
||||
"when": "scmProvider == gsd"
|
||||
}
|
||||
]
|
||||
},
|
||||
"chatParticipants": [
|
||||
|
|
@ -276,7 +361,7 @@
|
|||
},
|
||||
"gsd.showProgressNotifications": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"default": false,
|
||||
"description": "Show progress notification while the agent is working"
|
||||
},
|
||||
"gsd.activityFeedMaxItems": {
|
||||
|
|
@ -297,6 +382,17 @@
|
|||
"minimum": 50,
|
||||
"maximum": 95,
|
||||
"description": "Context window usage percentage that triggers a warning"
|
||||
},
|
||||
"gsd.approvalMode": {
|
||||
"type": "string",
|
||||
"default": "auto-approve",
|
||||
"enum": ["auto-approve", "ask", "plan-only"],
|
||||
"enumDescriptions": [
|
||||
"Agent runs freely without prompts",
|
||||
"Prompt before file changes and commands",
|
||||
"Read-only mode — agent can analyze but not modify"
|
||||
],
|
||||
"description": "Approval mode for agent actions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
295
vscode-extension/src/change-tracker.ts
Normal file
295
vscode-extension/src/change-tracker.ts
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
import * as vscode from "vscode";
|
||||
import * as fs from "node:fs";
|
||||
import type { GsdClient, AgentEvent } from "./gsd-client.js";
|
||||
|
||||
export interface FileSnapshot {
|
||||
uri: vscode.Uri;
|
||||
originalContent: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface Checkpoint {
|
||||
id: number;
|
||||
label: string;
|
||||
timestamp: number;
|
||||
/** Map of file path → original content at checkpoint creation time */
|
||||
snapshots: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks file changes made by the GSD agent. Stores original file content
|
||||
* before the agent modifies it, enabling diff views, SCM integration,
|
||||
* and checkpoint/rollback functionality.
|
||||
*/
|
||||
export class GsdChangeTracker implements vscode.Disposable {
|
||||
/** file path → original content (before first agent modification this session) */
|
||||
private originals = new Map<string, string>();
|
||||
/** Set of file paths modified in the current agent turn */
|
||||
private currentTurnFiles = new Set<string>();
|
||||
/** Ordered list of checkpoints */
|
||||
private _checkpoints: Checkpoint[] = [];
|
||||
private nextCheckpointId = 1;
|
||||
/** toolUseId → file path for in-flight tool executions */
|
||||
private pendingTools = new Map<string, string>();
|
||||
/** Whether the current turn has been described in the checkpoint label */
|
||||
private turnDescribed = false;
|
||||
|
||||
private readonly _onDidChange = new vscode.EventEmitter<string[]>();
|
||||
/** Fires when the set of tracked files changes. Payload is array of changed file paths. */
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private readonly _onCheckpointChange = new vscode.EventEmitter<void>();
|
||||
readonly onCheckpointChange = this._onCheckpointChange.event;
|
||||
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly client: GsdClient) {
|
||||
this.disposables.push(this._onDidChange, this._onCheckpointChange);
|
||||
|
||||
this.disposables.push(
|
||||
client.onEvent((evt) => this.handleEvent(evt)),
|
||||
client.onConnectionChange((connected) => {
|
||||
if (!connected) {
|
||||
this.reset();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** All file paths that have been modified by the agent */
|
||||
get modifiedFiles(): string[] {
|
||||
return [...this.originals.keys()];
|
||||
}
|
||||
|
||||
/** Get the original content of a file (before agent first modified it) */
|
||||
getOriginal(filePath: string): string | undefined {
|
||||
return this.originals.get(filePath);
|
||||
}
|
||||
|
||||
/** Whether the tracker has any modifications */
|
||||
get hasChanges(): boolean {
|
||||
return this.originals.size > 0;
|
||||
}
|
||||
|
||||
/** Current checkpoints (newest first) */
|
||||
get checkpoints(): readonly Checkpoint[] {
|
||||
return this._checkpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard agent changes to a single file — restore original content.
|
||||
* Returns true if the file was restored.
|
||||
*/
|
||||
async discardFile(filePath: string): Promise<boolean> {
|
||||
const original = this.originals.get(filePath);
|
||||
if (original === undefined) return false;
|
||||
|
||||
try {
|
||||
await fs.promises.writeFile(filePath, original, "utf8");
|
||||
this.originals.delete(filePath);
|
||||
this._onDidChange.fire([filePath]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discard all agent changes — restore all files to their original state.
|
||||
*/
|
||||
async discardAll(): Promise<number> {
|
||||
let count = 0;
|
||||
const paths = [...this.originals.keys()];
|
||||
for (const filePath of paths) {
|
||||
if (await this.discardFile(filePath)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept changes to a file — remove from tracking (keep the current content).
|
||||
*/
|
||||
acceptFile(filePath: string): void {
|
||||
if (this.originals.delete(filePath)) {
|
||||
this._onDidChange.fire([filePath]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept all changes — clear all tracking.
|
||||
*/
|
||||
acceptAll(): void {
|
||||
const paths = [...this.originals.keys()];
|
||||
this.originals.clear();
|
||||
if (paths.length > 0) {
|
||||
this._onDidChange.fire(paths);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all files to a checkpoint state.
|
||||
*/
|
||||
async restoreCheckpoint(checkpointId: number): Promise<number> {
|
||||
const idx = this._checkpoints.findIndex((c) => c.id === checkpointId);
|
||||
if (idx === -1) return 0;
|
||||
|
||||
const checkpoint = this._checkpoints[idx];
|
||||
let count = 0;
|
||||
|
||||
for (const [filePath, content] of checkpoint.snapshots) {
|
||||
try {
|
||||
await fs.promises.writeFile(filePath, content, "utf8");
|
||||
count++;
|
||||
} catch {
|
||||
// skip files that can't be restored
|
||||
}
|
||||
}
|
||||
|
||||
// Reset originals to the checkpoint state
|
||||
this.originals = new Map(checkpoint.snapshots);
|
||||
|
||||
// Remove all checkpoints after this one
|
||||
this._checkpoints = this._checkpoints.slice(0, idx);
|
||||
|
||||
this._onDidChange.fire([...checkpoint.snapshots.keys()]);
|
||||
this._onCheckpointChange.fire();
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Clear all tracking state */
|
||||
reset(): void {
|
||||
const paths = [...this.originals.keys()];
|
||||
this.originals.clear();
|
||||
this.currentTurnFiles.clear();
|
||||
this.pendingTools.clear();
|
||||
this._checkpoints = [];
|
||||
this.nextCheckpointId = 1;
|
||||
if (paths.length > 0) {
|
||||
this._onDidChange.fire(paths);
|
||||
}
|
||||
this._onCheckpointChange.fire();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private handleEvent(evt: AgentEvent): void {
|
||||
switch (evt.type) {
|
||||
case "agent_start":
|
||||
this.createCheckpoint();
|
||||
this.currentTurnFiles.clear();
|
||||
this.turnDescribed = false;
|
||||
break;
|
||||
|
||||
case "tool_execution_start": {
|
||||
const toolName = String(evt.toolName ?? "");
|
||||
const toolInput = (evt.toolInput ?? {}) as Record<string, unknown>;
|
||||
const toolUseId = String(evt.toolUseId ?? "");
|
||||
|
||||
// Update checkpoint label with first action description
|
||||
if (!this.turnDescribed) {
|
||||
this.turnDescribed = true;
|
||||
this.updateLatestCheckpointLabel(describeAction(toolName, toolInput));
|
||||
}
|
||||
|
||||
if (toolName !== "Write" && toolName !== "Edit") break;
|
||||
|
||||
const filePath = String(toolInput.file_path ?? toolInput.path ?? "");
|
||||
|
||||
if (!filePath) break;
|
||||
|
||||
// Store the original content before the agent modifies it
|
||||
// Only capture on FIRST modification (don't overwrite)
|
||||
if (!this.originals.has(filePath)) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
this.originals.set(filePath, content);
|
||||
} else {
|
||||
// File doesn't exist yet — original is "empty" (new file)
|
||||
this.originals.set(filePath, "");
|
||||
}
|
||||
} catch {
|
||||
// Can't read file, skip tracking
|
||||
}
|
||||
}
|
||||
|
||||
if (toolUseId) {
|
||||
this.pendingTools.set(toolUseId, filePath);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_end": {
|
||||
const toolUseId = String(evt.toolUseId ?? "");
|
||||
const filePath = this.pendingTools.get(toolUseId);
|
||||
if (filePath) {
|
||||
this.pendingTools.delete(toolUseId);
|
||||
this.currentTurnFiles.add(filePath);
|
||||
this._onDidChange.fire([filePath]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createCheckpoint(): void {
|
||||
const now = Date.now();
|
||||
const time = new Date(now).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
const fileCount = this.originals.size;
|
||||
const label = fileCount > 0
|
||||
? `${time} (${fileCount} file${fileCount !== 1 ? "s" : ""} tracked)`
|
||||
: `${time} (start)`;
|
||||
|
||||
const checkpoint: Checkpoint = {
|
||||
id: this.nextCheckpointId++,
|
||||
label,
|
||||
timestamp: now,
|
||||
snapshots: new Map(this.originals),
|
||||
};
|
||||
this._checkpoints.push(checkpoint);
|
||||
this._onCheckpointChange.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the label of the latest checkpoint with a description
|
||||
* of the first action taken (called after first tool execution in a turn).
|
||||
*/
|
||||
private updateLatestCheckpointLabel(description: string): void {
|
||||
if (this._checkpoints.length === 0) return;
|
||||
const latest = this._checkpoints[this._checkpoints.length - 1];
|
||||
const time = new Date(latest.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
latest.label = `${time} — ${description}`;
|
||||
this._onCheckpointChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
function describeAction(toolName: string, input: Record<string, unknown>): string {
|
||||
switch (toolName) {
|
||||
case "Read": {
|
||||
const p = String(input.file_path ?? input.path ?? "");
|
||||
return `Read ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Write": {
|
||||
const p = String(input.file_path ?? "");
|
||||
return `Write ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Edit": {
|
||||
const p = String(input.file_path ?? "");
|
||||
return `Edit ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Bash":
|
||||
return `$ ${String(input.command ?? "").slice(0, 40)}`;
|
||||
case "Grep":
|
||||
return `Grep: ${String(input.pattern ?? "").slice(0, 30)}`;
|
||||
case "Glob":
|
||||
return `Glob: ${String(input.pattern ?? "").slice(0, 30)}`;
|
||||
default:
|
||||
return toolName;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,21 @@ export function registerChatParticipant(
|
|||
message = `${fileContext}\n\n${message}`;
|
||||
}
|
||||
|
||||
// Auto-include editor selection if present and not already referenced
|
||||
const selectionContext = getSelectionContext();
|
||||
if (selectionContext) {
|
||||
message = `${selectionContext}\n\n${message}`;
|
||||
}
|
||||
|
||||
// Auto-include diagnostics for the active file if the prompt mentions "fix", "error", "problem", "warning"
|
||||
const fixKeywords = /\b(fix|error|problem|warning|issue|bug|lint|diagnos)/i;
|
||||
if (fixKeywords.test(message)) {
|
||||
const diagContext = getActiveDiagnosticsContext();
|
||||
if (diagContext) {
|
||||
message = `${message}\n\n${diagContext}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Track streaming state
|
||||
let agentDone = false;
|
||||
let totalInputTokens = 0;
|
||||
|
|
@ -281,3 +296,42 @@ function resolveFileUri(fp: string): vscode.Uri | null {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current editor selection as context, if any text is selected.
|
||||
*/
|
||||
function getSelectionContext(): string | null {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.selection.isEmpty) return null;
|
||||
|
||||
const selection = editor.document.getText(editor.selection);
|
||||
if (!selection.trim()) return null;
|
||||
|
||||
const relativePath = vscode.workspace.asRelativePath(editor.document.uri);
|
||||
const { start, end } = editor.selection;
|
||||
return `Selected code in \`${relativePath}\` (lines ${start.line + 1}-${end.line + 1}):\n\`\`\`\n${selection}\n\`\`\``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get diagnostics (errors/warnings) for the active editor file.
|
||||
*/
|
||||
function getActiveDiagnosticsContext(): string | null {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return null;
|
||||
|
||||
const diagnostics = vscode.languages.getDiagnostics(editor.document.uri);
|
||||
const significant = diagnostics.filter(
|
||||
(d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning,
|
||||
);
|
||||
if (significant.length === 0) return null;
|
||||
|
||||
const relativePath = vscode.workspace.asRelativePath(editor.document.uri);
|
||||
const lines = [`Current diagnostics in \`${relativePath}\`:`];
|
||||
for (const d of significant) {
|
||||
const sev = d.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning";
|
||||
const line = d.range.start.line + 1;
|
||||
const source = d.source ? ` [${d.source}]` : "";
|
||||
lines.push(`- ${sev} (line ${line}): ${d.message}${source}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
|
|||
55
vscode-extension/src/checkpoints.ts
Normal file
55
vscode-extension/src/checkpoints.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as vscode from "vscode";
|
||||
import type { GsdChangeTracker, Checkpoint } from "./change-tracker.js";
|
||||
|
||||
/**
|
||||
* TreeDataProvider that shows agent checkpoints (one per agent turn).
|
||||
* Each checkpoint can be restored to revert all file changes since that point.
|
||||
*/
|
||||
export class GsdCheckpointProvider implements vscode.TreeDataProvider<Checkpoint>, vscode.Disposable {
|
||||
public static readonly viewId = "gsd-checkpoints";
|
||||
|
||||
private readonly _onDidChangeTreeData = new vscode.EventEmitter<void>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly tracker: GsdChangeTracker) {
|
||||
this.disposables.push(
|
||||
this._onDidChangeTreeData,
|
||||
tracker.onCheckpointChange(() => this._onDidChangeTreeData.fire()),
|
||||
);
|
||||
}
|
||||
|
||||
getTreeItem(checkpoint: Checkpoint): vscode.TreeItem {
|
||||
const fileCount = checkpoint.snapshots.size;
|
||||
const time = new Date(checkpoint.timestamp);
|
||||
const timeStr = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
|
||||
const item = new vscode.TreeItem(
|
||||
checkpoint.label,
|
||||
vscode.TreeItemCollapsibleState.None,
|
||||
);
|
||||
item.description = `${timeStr} (${fileCount} file${fileCount !== 1 ? "s" : ""})`;
|
||||
item.iconPath = new vscode.ThemeIcon("history");
|
||||
item.tooltip = `Checkpoint: ${checkpoint.label}\nTime: ${time.toLocaleString()}\nFiles tracked: ${fileCount}\n\nClick to restore to this point`;
|
||||
item.contextValue = "checkpoint";
|
||||
item.command = {
|
||||
command: "gsd.restoreCheckpoint",
|
||||
title: "Restore Checkpoint",
|
||||
arguments: [checkpoint.id],
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(): Checkpoint[] {
|
||||
// Show newest first
|
||||
return [...this.tracker.checkpoints].reverse();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
142
vscode-extension/src/diagnostics.ts
Normal file
142
vscode-extension/src/diagnostics.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import * as vscode from "vscode";
|
||||
import type { GsdClient } from "./gsd-client.js";
|
||||
|
||||
/**
|
||||
* Integrates with VS Code's diagnostic system:
|
||||
* - Reads diagnostics (errors/warnings) from the Problems panel and sends them to the agent
|
||||
* - Provides a DiagnosticCollection for the agent to surface its own findings
|
||||
*/
|
||||
export class GsdDiagnosticBridge implements vscode.Disposable {
|
||||
private readonly collection: vscode.DiagnosticCollection;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly client: GsdClient) {
|
||||
this.collection = vscode.languages.createDiagnosticCollection("gsd");
|
||||
this.disposables.push(this.collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all diagnostics for the active file and send them to the agent
|
||||
* as a "fix these problems" prompt.
|
||||
*/
|
||||
async fixProblemsInFile(): Promise<void> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage("No active file to fix.");
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = editor.document.uri;
|
||||
const diagnostics = vscode.languages.getDiagnostics(uri);
|
||||
|
||||
if (diagnostics.length === 0) {
|
||||
vscode.window.showInformationMessage("No problems found in this file.");
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = vscode.workspace.asRelativePath(uri);
|
||||
const problemText = formatDiagnostics(fileName, diagnostics);
|
||||
|
||||
const prompt = [
|
||||
`Fix the following problems in \`${fileName}\`:`,
|
||||
"",
|
||||
problemText,
|
||||
"",
|
||||
"Fix all of these issues. Show me the changes.",
|
||||
].join("\n");
|
||||
|
||||
await this.client.sendPrompt(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all diagnostics across the workspace (errors only) and send
|
||||
* them to the agent as a "fix all errors" prompt.
|
||||
*/
|
||||
async fixAllProblems(): Promise<void> {
|
||||
const allDiagnostics = vscode.languages.getDiagnostics();
|
||||
const errorFiles: { fileName: string; diagnostics: vscode.Diagnostic[] }[] = [];
|
||||
|
||||
for (const [uri, diagnostics] of allDiagnostics) {
|
||||
// Only include errors and warnings, skip hints/info
|
||||
const significant = diagnostics.filter(
|
||||
(d) => d.severity === vscode.DiagnosticSeverity.Error || d.severity === vscode.DiagnosticSeverity.Warning,
|
||||
);
|
||||
if (significant.length > 0) {
|
||||
errorFiles.push({
|
||||
fileName: vscode.workspace.asRelativePath(uri),
|
||||
diagnostics: significant,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (errorFiles.length === 0) {
|
||||
vscode.window.showInformationMessage("No errors or warnings found in the workspace.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cap at 20 files to avoid overwhelming the agent
|
||||
const capped = errorFiles.slice(0, 20);
|
||||
const totalProblems = capped.reduce((sum, f) => sum + f.diagnostics.length, 0);
|
||||
|
||||
const sections = capped.map((f) => formatDiagnostics(f.fileName, f.diagnostics));
|
||||
|
||||
const prompt = [
|
||||
`Fix the following ${totalProblems} problems across ${capped.length} file${capped.length > 1 ? "s" : ""}:`,
|
||||
"",
|
||||
...sections,
|
||||
"",
|
||||
"Fix all of these issues.",
|
||||
].join("\n");
|
||||
|
||||
await this.client.sendPrompt(prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a GSD diagnostic (agent finding) to a file.
|
||||
* Can be used to surface agent review findings in the Problems panel.
|
||||
*/
|
||||
addFinding(
|
||||
uri: vscode.Uri,
|
||||
range: vscode.Range,
|
||||
message: string,
|
||||
severity: vscode.DiagnosticSeverity = vscode.DiagnosticSeverity.Warning,
|
||||
): void {
|
||||
const existing = this.collection.get(uri) ?? [];
|
||||
const diagnostic = new vscode.Diagnostic(range, message, severity);
|
||||
diagnostic.source = "GSD Agent";
|
||||
this.collection.set(uri, [...existing, diagnostic]);
|
||||
}
|
||||
|
||||
/** Clear all GSD diagnostics */
|
||||
clearFindings(): void {
|
||||
this.collection.clear();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatDiagnostics(fileName: string, diagnostics: vscode.Diagnostic[]): string {
|
||||
const lines = [`**${fileName}**`];
|
||||
for (const d of diagnostics) {
|
||||
const severity = severityLabel(d.severity);
|
||||
const line = d.range.start.line + 1;
|
||||
const col = d.range.start.character + 1;
|
||||
const source = d.source ? ` [${d.source}]` : "";
|
||||
lines.push(` - ${severity} (line ${line}:${col}): ${d.message}${source}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function severityLabel(severity: vscode.DiagnosticSeverity): string {
|
||||
switch (severity) {
|
||||
case vscode.DiagnosticSeverity.Error: return "Error";
|
||||
case vscode.DiagnosticSeverity.Warning: return "Warning";
|
||||
case vscode.DiagnosticSeverity.Information: return "Info";
|
||||
case vscode.DiagnosticSeverity.Hint: return "Hint";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,24 @@ import { GsdConversationHistoryPanel } from "./conversation-history.js";
|
|||
import { GsdSlashCompletionProvider } from "./slash-completion.js";
|
||||
import { GsdCodeLensProvider } from "./code-lens.js";
|
||||
import { GsdActivityFeedProvider } from "./activity-feed.js";
|
||||
import { GsdChangeTracker } from "./change-tracker.js";
|
||||
import { GsdScmProvider } from "./scm-provider.js";
|
||||
import { GsdDiagnosticBridge } from "./diagnostics.js";
|
||||
import { GsdLineDecorationManager } from "./line-decorations.js";
|
||||
import { GsdGitIntegration } from "./git-integration.js";
|
||||
import { GsdPermissionManager } from "./permissions.js";
|
||||
|
||||
let client: GsdClient | undefined;
|
||||
let sidebarProvider: GsdSidebarProvider | undefined;
|
||||
let fileDecorations: GsdFileDecorationProvider | undefined;
|
||||
let sessionTreeProvider: GsdSessionTreeProvider | undefined;
|
||||
let activityFeedProvider: GsdActivityFeedProvider | undefined;
|
||||
let changeTracker: GsdChangeTracker | undefined;
|
||||
let scmProvider: GsdScmProvider | undefined;
|
||||
let diagnosticBridge: GsdDiagnosticBridge | undefined;
|
||||
let lineDecorations: GsdLineDecorationManager | undefined;
|
||||
let gitIntegration: GsdGitIntegration | undefined;
|
||||
let permissionManager: GsdPermissionManager | undefined;
|
||||
|
||||
function requireConnected(): boolean {
|
||||
if (!client?.isConnected) {
|
||||
|
|
@ -128,6 +140,34 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||
vscode.window.registerTreeDataProvider(GsdActivityFeedProvider.viewId, activityFeedProvider),
|
||||
);
|
||||
|
||||
// -- Change tracker & SCM provider -------------------------------------
|
||||
|
||||
changeTracker = new GsdChangeTracker(client);
|
||||
context.subscriptions.push(changeTracker);
|
||||
|
||||
scmProvider = new GsdScmProvider(changeTracker, cwd);
|
||||
context.subscriptions.push(scmProvider);
|
||||
|
||||
// -- Diagnostics -------------------------------------------------------
|
||||
|
||||
diagnosticBridge = new GsdDiagnosticBridge(client);
|
||||
context.subscriptions.push(diagnosticBridge);
|
||||
|
||||
// -- Line-level decorations --------------------------------------------
|
||||
|
||||
lineDecorations = new GsdLineDecorationManager(changeTracker!);
|
||||
context.subscriptions.push(lineDecorations);
|
||||
|
||||
// -- Git integration ---------------------------------------------------
|
||||
|
||||
gitIntegration = new GsdGitIntegration(changeTracker!, cwd);
|
||||
context.subscriptions.push(gitIntegration);
|
||||
|
||||
// -- Permissions -------------------------------------------------------
|
||||
|
||||
permissionManager = new GsdPermissionManager(client);
|
||||
context.subscriptions.push(permissionManager);
|
||||
|
||||
// -- Progress notifications --------------------------------------------
|
||||
|
||||
let currentProgress: { resolve: () => void } | undefined;
|
||||
|
|
@ -789,6 +829,135 @@ export function activate(context: vscode.ExtensionContext): void {
|
|||
}),
|
||||
);
|
||||
|
||||
// -- SCM commands -------------------------------------------------------
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.acceptAllChanges", () => {
|
||||
changeTracker?.acceptAll();
|
||||
vscode.window.showInformationMessage("All agent changes accepted.");
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.discardAllChanges", async () => {
|
||||
if (!changeTracker?.hasChanges) {
|
||||
vscode.window.showInformationMessage("No agent changes to discard.");
|
||||
return;
|
||||
}
|
||||
const confirm = await vscode.window.showWarningMessage(
|
||||
`Discard all agent changes (${changeTracker.modifiedFiles.length} files)?`,
|
||||
{ modal: true },
|
||||
"Discard",
|
||||
);
|
||||
if (confirm === "Discard") {
|
||||
const count = await changeTracker.discardAll();
|
||||
vscode.window.showInformationMessage(`Reverted ${count} file${count !== 1 ? "s" : ""}.`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.discardFileChanges", async (resourceState: vscode.SourceControlResourceState) => {
|
||||
if (!changeTracker || !resourceState?.resourceUri) return;
|
||||
const filePath = resourceState.resourceUri.fsPath;
|
||||
const success = await changeTracker.discardFile(filePath);
|
||||
if (success) {
|
||||
vscode.window.showInformationMessage(`Reverted ${vscode.workspace.asRelativePath(filePath)}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.acceptFileChanges", (resourceState: vscode.SourceControlResourceState) => {
|
||||
if (!changeTracker || !resourceState?.resourceUri) return;
|
||||
changeTracker.acceptFile(resourceState.resourceUri.fsPath);
|
||||
}),
|
||||
);
|
||||
|
||||
// -- Checkpoint commands ------------------------------------------------
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.restoreCheckpoint", async (checkpointId: number) => {
|
||||
if (!changeTracker) return;
|
||||
const checkpoint = changeTracker.checkpoints.find((c) => c.id === checkpointId);
|
||||
if (!checkpoint) return;
|
||||
|
||||
const confirm = await vscode.window.showWarningMessage(
|
||||
`Restore to "${checkpoint.label}"? This will revert files to their state at ${new Date(checkpoint.timestamp).toLocaleTimeString()}.`,
|
||||
{ modal: true },
|
||||
"Restore",
|
||||
);
|
||||
if (confirm === "Restore") {
|
||||
const count = await changeTracker.restoreCheckpoint(checkpointId);
|
||||
vscode.window.showInformationMessage(`Restored ${count} file${count !== 1 ? "s" : ""} to checkpoint.`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// -- Diagnostic commands ------------------------------------------------
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.fixProblemsInFile", async () => {
|
||||
if (!requireConnected()) return;
|
||||
try {
|
||||
await diagnosticBridge!.fixProblemsInFile();
|
||||
} catch (err) {
|
||||
handleError(err, "Failed to fix problems");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.fixAllProblems", async () => {
|
||||
if (!requireConnected()) return;
|
||||
try {
|
||||
await diagnosticBridge!.fixAllProblems();
|
||||
} catch (err) {
|
||||
handleError(err, "Failed to fix problems");
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.clearDiagnostics", () => {
|
||||
diagnosticBridge?.clearFindings();
|
||||
}),
|
||||
);
|
||||
|
||||
// -- Permission commands ------------------------------------------------
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.cycleApprovalMode", () => {
|
||||
permissionManager?.cycleMode();
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.selectApprovalMode", () => {
|
||||
permissionManager?.selectMode();
|
||||
}),
|
||||
);
|
||||
|
||||
// -- Git commands -------------------------------------------------------
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.commitAgentChanges", () => {
|
||||
gitIntegration?.commitAgentChanges();
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.createAgentBranch", () => {
|
||||
gitIntegration?.createAgentBranch();
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("gsd.showAgentDiff", () => {
|
||||
gitIntegration?.showAgentDiff();
|
||||
}),
|
||||
);
|
||||
|
||||
// -- Auto-start ---------------------------------------------------------
|
||||
|
||||
if (config.get<boolean>("autoStart", false)) {
|
||||
|
|
@ -802,9 +971,21 @@ export function deactivate(): void {
|
|||
fileDecorations?.dispose();
|
||||
sessionTreeProvider?.dispose();
|
||||
activityFeedProvider?.dispose();
|
||||
changeTracker?.dispose();
|
||||
scmProvider?.dispose();
|
||||
diagnosticBridge?.dispose();
|
||||
lineDecorations?.dispose();
|
||||
gitIntegration?.dispose();
|
||||
permissionManager?.dispose();
|
||||
client = undefined;
|
||||
sidebarProvider = undefined;
|
||||
fileDecorations = undefined;
|
||||
sessionTreeProvider = undefined;
|
||||
activityFeedProvider = undefined;
|
||||
changeTracker = undefined;
|
||||
scmProvider = undefined;
|
||||
diagnosticBridge = undefined;
|
||||
lineDecorations = undefined;
|
||||
gitIntegration = undefined;
|
||||
permissionManager = undefined;
|
||||
}
|
||||
|
|
|
|||
122
vscode-extension/src/git-integration.ts
Normal file
122
vscode-extension/src/git-integration.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import * as vscode from "vscode";
|
||||
import { exec } from "node:child_process";
|
||||
import type { GsdChangeTracker } from "./change-tracker.js";
|
||||
|
||||
/**
|
||||
* Provides git integration for agent changes — commit, branch, and diff.
|
||||
*/
|
||||
export class GsdGitIntegration implements vscode.Disposable {
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly tracker: GsdChangeTracker,
|
||||
private readonly cwd: string,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Commit all files modified by the agent with a user-provided message.
|
||||
*/
|
||||
async commitAgentChanges(): Promise<void> {
|
||||
const files = this.tracker.modifiedFiles;
|
||||
if (files.length === 0) {
|
||||
vscode.window.showInformationMessage("No agent changes to commit.");
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultMsg = `feat: agent changes (${files.length} file${files.length !== 1 ? "s" : ""})`;
|
||||
const message = await vscode.window.showInputBox({
|
||||
prompt: "Commit message for agent changes",
|
||||
value: defaultMsg,
|
||||
placeHolder: "feat: describe the changes",
|
||||
});
|
||||
if (!message) return;
|
||||
|
||||
try {
|
||||
// Stage the modified files
|
||||
await this.git(`add ${files.map((f) => `"${f}"`).join(" ")}`);
|
||||
// Commit
|
||||
await this.git(`commit -m "${message.replace(/"/g, '\\"')}"`);
|
||||
|
||||
// Accept all changes (clear tracking since they're committed)
|
||||
this.tracker.acceptAll();
|
||||
|
||||
vscode.window.showInformationMessage(`Committed ${files.length} file${files.length !== 1 ? "s" : ""}.`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`Git commit failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new branch for agent work and switch to it.
|
||||
*/
|
||||
async createAgentBranch(): Promise<void> {
|
||||
const branchName = await vscode.window.showInputBox({
|
||||
prompt: "Branch name for agent work",
|
||||
placeHolder: "feat/agent-changes",
|
||||
validateInput: (value) => {
|
||||
if (!value.trim()) return "Branch name is required";
|
||||
if (/\s/.test(value)) return "Branch name cannot contain spaces";
|
||||
return null;
|
||||
},
|
||||
});
|
||||
if (!branchName) return;
|
||||
|
||||
try {
|
||||
await this.git(`checkout -b "${branchName}"`);
|
||||
vscode.window.showInformationMessage(`Created and switched to branch: ${branchName}`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`Failed to create branch: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a git diff of all agent-modified files.
|
||||
*/
|
||||
async showAgentDiff(): Promise<void> {
|
||||
const files = this.tracker.modifiedFiles;
|
||||
if (files.length === 0) {
|
||||
vscode.window.showInformationMessage("No agent changes to diff.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const diff = await this.git("diff");
|
||||
if (!diff.trim()) {
|
||||
// Files may be untracked — show status instead
|
||||
const status = await this.git("status --short");
|
||||
const channel = vscode.window.createOutputChannel("GSD Git Diff");
|
||||
channel.appendLine("# Agent-modified files (unstaged):");
|
||||
channel.appendLine(status);
|
||||
channel.show();
|
||||
} else {
|
||||
const channel = vscode.window.createOutputChannel("GSD Git Diff");
|
||||
channel.clear();
|
||||
channel.appendLine(diff);
|
||||
channel.show();
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
vscode.window.showErrorMessage(`Git diff failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private git(args: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`git ${args}`, { cwd: this.cwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(new Error(stderr.trim() || err.message));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -123,11 +123,10 @@ export class GsdClient implements vscode.Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
const proc = spawn(this.binaryPath, ["--mode", "rpc", "--no-session"], {
|
||||
const proc = spawn(this.binaryPath, ["--mode", "rpc"], {
|
||||
cwd: this.cwd,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
env: { ...process.env },
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
this.process = proc;
|
||||
|
||||
|
|
@ -580,10 +579,104 @@ export class GsdClient implements vscode.Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
// Extension UI request — agent needs user input
|
||||
if (data.type === "extension_ui_request" && typeof data.id === "string") {
|
||||
void this.handleUIRequest(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Streaming event
|
||||
this._onEvent.fire(data as AgentEvent);
|
||||
}
|
||||
|
||||
private async handleUIRequest(request: Record<string, unknown>): Promise<void> {
|
||||
const id = request.id as string;
|
||||
const method = request.method as string;
|
||||
|
||||
try {
|
||||
switch (method) {
|
||||
case "select": {
|
||||
const options = (request.options as string[]) ?? [];
|
||||
const title = String(request.title ?? "Select");
|
||||
const allowMultiple = request.allowMultiple === true;
|
||||
|
||||
if (allowMultiple) {
|
||||
const picked = await vscode.window.showQuickPick(options, {
|
||||
title,
|
||||
canPickMany: true,
|
||||
});
|
||||
if (picked) {
|
||||
this.sendRaw({ type: "extension_ui_response", id, values: picked });
|
||||
} else {
|
||||
this.sendRaw({ type: "extension_ui_response", id, cancelled: true });
|
||||
}
|
||||
} else {
|
||||
const picked = await vscode.window.showQuickPick(options, { title });
|
||||
if (picked) {
|
||||
this.sendRaw({ type: "extension_ui_response", id, value: picked });
|
||||
} else {
|
||||
this.sendRaw({ type: "extension_ui_response", id, cancelled: true });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "confirm": {
|
||||
const title = String(request.title ?? "Confirm");
|
||||
const message = String(request.message ?? "");
|
||||
const result = await vscode.window.showInformationMessage(
|
||||
`${title}: ${message}`,
|
||||
{ modal: true },
|
||||
"Yes",
|
||||
"No",
|
||||
);
|
||||
this.sendRaw({ type: "extension_ui_response", id, confirmed: result === "Yes" });
|
||||
break;
|
||||
}
|
||||
|
||||
case "input": {
|
||||
const title = String(request.title ?? "Input");
|
||||
const placeholder = String(request.placeholder ?? "");
|
||||
const value = await vscode.window.showInputBox({ title, placeHolder: placeholder });
|
||||
if (value !== undefined) {
|
||||
this.sendRaw({ type: "extension_ui_response", id, value });
|
||||
} else {
|
||||
this.sendRaw({ type: "extension_ui_response", id, cancelled: true });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "notify": {
|
||||
const message = String(request.message ?? "");
|
||||
const notifyType = String(request.notifyType ?? "info");
|
||||
if (notifyType === "error") {
|
||||
vscode.window.showErrorMessage(`GSD: ${message}`);
|
||||
} else if (notifyType === "warning") {
|
||||
vscode.window.showWarningMessage(`GSD: ${message}`);
|
||||
} else {
|
||||
vscode.window.showInformationMessage(`GSD: ${message}`);
|
||||
}
|
||||
// Notify doesn't need a response
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown method — cancel to unblock the agent
|
||||
this.sendRaw({ type: "extension_ui_response", id, cancelled: true });
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// On error, cancel to unblock
|
||||
this.sendRaw({ type: "extension_ui_response", id, cancelled: true });
|
||||
}
|
||||
}
|
||||
|
||||
private sendRaw(data: Record<string, unknown>): void {
|
||||
if (this.process?.stdin) {
|
||||
this.process.stdin.write(JSON.stringify(data) + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
private send(command: Record<string, unknown>): Promise<RpcResponse> {
|
||||
if (!this.process?.stdin) {
|
||||
return Promise.reject(new Error("GSD client not started"));
|
||||
|
|
|
|||
130
vscode-extension/src/line-decorations.ts
Normal file
130
vscode-extension/src/line-decorations.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import * as vscode from "vscode";
|
||||
import type { GsdChangeTracker } from "./change-tracker.js";
|
||||
|
||||
/**
|
||||
* Provides line-level editor decorations for files modified by the GSD agent.
|
||||
* Shows subtle background highlights on changed lines and gutter icons.
|
||||
*/
|
||||
export class GsdLineDecorationManager implements vscode.Disposable {
|
||||
private readonly addedDecoration: vscode.TextEditorDecorationType;
|
||||
private readonly modifiedDecoration: vscode.TextEditorDecorationType;
|
||||
private readonly gutterDecoration: vscode.TextEditorDecorationType;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly tracker: GsdChangeTracker) {
|
||||
this.addedDecoration = vscode.window.createTextEditorDecorationType({
|
||||
isWholeLine: true,
|
||||
backgroundColor: "rgba(78, 201, 176, 0.07)",
|
||||
overviewRulerColor: "rgba(78, 201, 176, 0.5)",
|
||||
overviewRulerLane: vscode.OverviewRulerLane.Left,
|
||||
});
|
||||
|
||||
this.modifiedDecoration = vscode.window.createTextEditorDecorationType({
|
||||
isWholeLine: true,
|
||||
backgroundColor: "rgba(204, 167, 0, 0.07)",
|
||||
overviewRulerColor: "rgba(204, 167, 0, 0.5)",
|
||||
overviewRulerLane: vscode.OverviewRulerLane.Left,
|
||||
});
|
||||
|
||||
this.gutterDecoration = vscode.window.createTextEditorDecorationType({
|
||||
gutterIconPath: new vscode.ThemeIcon("hubot").id, // fallback
|
||||
gutterIconSize: "contain",
|
||||
// Use a colored left border as a gutter indicator (more reliable than icons)
|
||||
borderWidth: "0 0 0 3px",
|
||||
borderStyle: "solid",
|
||||
borderColor: "rgba(78, 201, 176, 0.4)",
|
||||
});
|
||||
|
||||
this.disposables.push(
|
||||
this.addedDecoration,
|
||||
this.modifiedDecoration,
|
||||
this.gutterDecoration,
|
||||
);
|
||||
|
||||
// Refresh decorations when tracked files change
|
||||
this.disposables.push(
|
||||
tracker.onDidChange(() => this.refreshAll()),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => this.refreshAll()),
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && e.document === editor.document) {
|
||||
this.refreshEditor(editor);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private refreshAll(): void {
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
this.refreshEditor(editor);
|
||||
}
|
||||
}
|
||||
|
||||
private refreshEditor(editor: vscode.TextEditor): void {
|
||||
const filePath = editor.document.uri.fsPath;
|
||||
const original = this.tracker.getOriginal(filePath);
|
||||
|
||||
if (original === undefined) {
|
||||
// No tracked changes for this file — clear decorations
|
||||
editor.setDecorations(this.addedDecoration, []);
|
||||
editor.setDecorations(this.modifiedDecoration, []);
|
||||
editor.setDecorations(this.gutterDecoration, []);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLines = editor.document.getText().split("\n");
|
||||
const originalLines = original.split("\n");
|
||||
const { added, modified } = diffLines(originalLines, currentLines);
|
||||
|
||||
const addedRanges = added.map((line) => {
|
||||
const range = new vscode.Range(line, 0, line, currentLines[line]?.length ?? 0);
|
||||
return { range, hoverMessage: new vscode.MarkdownString("$(hubot) *Added by GSD Agent*") };
|
||||
});
|
||||
|
||||
const modifiedRanges = modified.map((line) => {
|
||||
const range = new vscode.Range(line, 0, line, currentLines[line]?.length ?? 0);
|
||||
return { range, hoverMessage: new vscode.MarkdownString("$(hubot) *Modified by GSD Agent*") };
|
||||
});
|
||||
|
||||
const gutterRanges = [...added, ...modified].map((line) => ({
|
||||
range: new vscode.Range(line, 0, line, 0),
|
||||
}));
|
||||
|
||||
editor.setDecorations(this.addedDecoration, addedRanges);
|
||||
editor.setDecorations(this.modifiedDecoration, modifiedRanges);
|
||||
editor.setDecorations(this.gutterDecoration, gutterRanges);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple line-level diff: compare original vs current line-by-line.
|
||||
* Returns arrays of line numbers that were added or modified.
|
||||
*/
|
||||
function diffLines(
|
||||
originalLines: string[],
|
||||
currentLines: string[],
|
||||
): { added: number[]; modified: number[] } {
|
||||
const added: number[] = [];
|
||||
const modified: number[] = [];
|
||||
|
||||
const maxShared = Math.min(originalLines.length, currentLines.length);
|
||||
|
||||
for (let i = 0; i < maxShared; i++) {
|
||||
if (originalLines[i] !== currentLines[i]) {
|
||||
modified.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Lines beyond original length are "added"
|
||||
for (let i = originalLines.length; i < currentLines.length; i++) {
|
||||
added.push(i);
|
||||
}
|
||||
|
||||
return { added, modified };
|
||||
}
|
||||
143
vscode-extension/src/permissions.ts
Normal file
143
vscode-extension/src/permissions.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import * as vscode from "vscode";
|
||||
import type { GsdClient, AgentEvent } from "./gsd-client.js";
|
||||
|
||||
type ApprovalMode = "ask" | "auto-approve" | "plan-only";
|
||||
|
||||
/**
|
||||
* Permission/approval system for agent actions.
|
||||
* Can be configured to prompt before file writes, command execution, etc.
|
||||
*/
|
||||
export class GsdPermissionManager implements vscode.Disposable {
|
||||
private _mode: ApprovalMode = "auto-approve";
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
private readonly _onModeChange = new vscode.EventEmitter<ApprovalMode>();
|
||||
readonly onModeChange = this._onModeChange.event;
|
||||
|
||||
constructor(private readonly client: GsdClient) {
|
||||
// Load saved mode from configuration
|
||||
this._mode = vscode.workspace.getConfiguration("gsd").get<ApprovalMode>("approvalMode", "auto-approve");
|
||||
|
||||
this.disposables.push(
|
||||
this._onModeChange,
|
||||
vscode.workspace.onDidChangeConfiguration((e) => {
|
||||
if (e.affectsConfiguration("gsd.approvalMode")) {
|
||||
this._mode = vscode.workspace.getConfiguration("gsd").get<ApprovalMode>("approvalMode", "auto-approve");
|
||||
this._onModeChange.fire(this._mode);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// If mode is "ask", intercept tool executions for write operations
|
||||
if (this._mode === "ask") {
|
||||
this.disposables.push(
|
||||
client.onEvent((evt) => this.handleEvent(evt)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get mode(): ApprovalMode {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through approval modes: auto-approve -> ask -> plan-only -> auto-approve
|
||||
*/
|
||||
async cycleMode(): Promise<void> {
|
||||
const modes: ApprovalMode[] = ["auto-approve", "ask", "plan-only"];
|
||||
const currentIdx = modes.indexOf(this._mode);
|
||||
this._mode = modes[(currentIdx + 1) % modes.length];
|
||||
|
||||
await vscode.workspace.getConfiguration("gsd").update("approvalMode", this._mode, vscode.ConfigurationTarget.Workspace);
|
||||
this._onModeChange.fire(this._mode);
|
||||
|
||||
const labels: Record<ApprovalMode, string> = {
|
||||
"auto-approve": "Auto-Approve (agent runs freely)",
|
||||
"ask": "Ask (prompt before file changes)",
|
||||
"plan-only": "Plan Only (read-only, no writes)",
|
||||
};
|
||||
vscode.window.showInformationMessage(`Approval mode: ${labels[this._mode]}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a QuickPick to select approval mode.
|
||||
*/
|
||||
async selectMode(): Promise<void> {
|
||||
const items: (vscode.QuickPickItem & { mode: ApprovalMode })[] = [
|
||||
{
|
||||
label: "$(check) Auto-Approve",
|
||||
description: "Agent runs freely without prompts",
|
||||
detail: "Best for trusted workflows. The agent can read, write, and execute without asking.",
|
||||
mode: "auto-approve",
|
||||
},
|
||||
{
|
||||
label: "$(shield) Ask",
|
||||
description: "Prompt before file changes",
|
||||
detail: "The agent will ask for approval before writing or editing files.",
|
||||
mode: "ask",
|
||||
},
|
||||
{
|
||||
label: "$(eye) Plan Only",
|
||||
description: "Read-only mode, no writes allowed",
|
||||
detail: "The agent can read and analyze but cannot modify files or run commands.",
|
||||
mode: "plan-only",
|
||||
},
|
||||
];
|
||||
|
||||
const selected = await vscode.window.showQuickPick(items, {
|
||||
placeHolder: `Current mode: ${this._mode}`,
|
||||
});
|
||||
|
||||
if (selected) {
|
||||
this._mode = selected.mode;
|
||||
await vscode.workspace.getConfiguration("gsd").update("approvalMode", this._mode, vscode.ConfigurationTarget.Workspace);
|
||||
this._onModeChange.fire(this._mode);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEvent(evt: AgentEvent): Promise<void> {
|
||||
if (this._mode !== "ask") return;
|
||||
if (evt.type !== "tool_execution_start") return;
|
||||
|
||||
const toolName = String(evt.toolName ?? "");
|
||||
if (toolName !== "Write" && toolName !== "Edit" && toolName !== "Bash") return;
|
||||
|
||||
const toolInput = (evt.toolInput ?? {}) as Record<string, unknown>;
|
||||
let description = "";
|
||||
|
||||
switch (toolName) {
|
||||
case "Write":
|
||||
case "Edit": {
|
||||
const filePath = String(toolInput.file_path ?? "");
|
||||
const shortPath = filePath.split(/[\\/]/).slice(-3).join("/");
|
||||
description = `${toolName}: ${shortPath}`;
|
||||
break;
|
||||
}
|
||||
case "Bash": {
|
||||
const cmd = String(toolInput.command ?? "").slice(0, 80);
|
||||
description = `Execute: ${cmd}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: In practice, the RPC protocol doesn't support blocking tool execution
|
||||
// for approval. This notification serves as awareness — the user sees what's
|
||||
// happening and can abort if needed. True blocking approval would require
|
||||
// protocol changes in the RPC server.
|
||||
vscode.window.showInformationMessage(
|
||||
`Agent: ${description}`,
|
||||
"OK",
|
||||
"Abort",
|
||||
).then((choice) => {
|
||||
if (choice === "Abort") {
|
||||
this.client.abort().catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
190
vscode-extension/src/plan-viewer.ts
Normal file
190
vscode-extension/src/plan-viewer.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import * as vscode from "vscode";
|
||||
import type { GsdClient, AgentEvent } from "./gsd-client.js";
|
||||
|
||||
interface PlanStep {
|
||||
id: number;
|
||||
tool: string;
|
||||
description: string;
|
||||
status: "pending" | "running" | "done" | "error";
|
||||
timestamp: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* TreeDataProvider that shows a plan-like view of agent tool executions.
|
||||
* Displays steps as they happen, showing what the agent is doing and
|
||||
* what it has completed — a live execution plan.
|
||||
*/
|
||||
export class GsdPlanViewerProvider implements vscode.TreeDataProvider<PlanStep>, vscode.Disposable {
|
||||
public static readonly viewId = "gsd-plan";
|
||||
|
||||
private readonly _onDidChangeTreeData = new vscode.EventEmitter<void>();
|
||||
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
private steps: PlanStep[] = [];
|
||||
private nextId = 0;
|
||||
private runningTools = new Map<string, number>(); // toolUseId -> step id
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(private readonly client: GsdClient) {
|
||||
this.disposables.push(
|
||||
this._onDidChangeTreeData,
|
||||
client.onEvent((evt) => this.handleEvent(evt)),
|
||||
client.onConnectionChange((connected) => {
|
||||
if (!connected) {
|
||||
this.steps = [];
|
||||
this.runningTools.clear();
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getTreeItem(step: PlanStep): vscode.TreeItem {
|
||||
const icon = stepIcon(step.status);
|
||||
const item = new vscode.TreeItem(step.description, vscode.TreeItemCollapsibleState.None);
|
||||
item.iconPath = icon;
|
||||
item.description = step.duration !== undefined ? `${step.duration}ms` : step.status === "running" ? "running..." : "";
|
||||
|
||||
const time = new Date(step.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
item.tooltip = `${step.tool}: ${step.description}\nStatus: ${step.status}\nTime: ${time}`;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
getChildren(): PlanStep[] {
|
||||
return this.steps;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.steps = [];
|
||||
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": {
|
||||
// Don't clear — keep history visible. Add a separator.
|
||||
if (this.steps.length > 0) {
|
||||
this.steps.push({
|
||||
id: this.nextId++,
|
||||
tool: "separator",
|
||||
description: "--- New Turn ---",
|
||||
status: "done",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
this.steps.push({
|
||||
id: this.nextId++,
|
||||
tool: "agent",
|
||||
description: "Agent started",
|
||||
status: "running",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
this._onDidChangeTreeData.fire();
|
||||
break;
|
||||
}
|
||||
|
||||
case "agent_end": {
|
||||
// Mark the agent step as done
|
||||
const agentStep = [...this.steps].reverse().find((s) => s.tool === "agent" && s.status === "running");
|
||||
if (agentStep) {
|
||||
agentStep.status = "done";
|
||||
agentStep.duration = Date.now() - agentStep.timestamp;
|
||||
agentStep.description = "Agent finished";
|
||||
}
|
||||
this._onDidChangeTreeData.fire();
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_start": {
|
||||
const toolName = String(evt.toolName ?? "");
|
||||
const toolInput = (evt.toolInput ?? {}) as Record<string, unknown>;
|
||||
const toolUseId = String(evt.toolUseId ?? "");
|
||||
const description = describeStep(toolName, toolInput);
|
||||
|
||||
const id = this.nextId++;
|
||||
this.steps.push({
|
||||
id,
|
||||
tool: toolName,
|
||||
description,
|
||||
status: "running",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (toolUseId) {
|
||||
this.runningTools.set(toolUseId, id);
|
||||
}
|
||||
|
||||
// Cap at 200 steps
|
||||
while (this.steps.length > 200) {
|
||||
this.steps.shift();
|
||||
}
|
||||
|
||||
this._onDidChangeTreeData.fire();
|
||||
break;
|
||||
}
|
||||
|
||||
case "tool_execution_end": {
|
||||
const toolUseId = String(evt.toolUseId ?? "");
|
||||
const stepId = this.runningTools.get(toolUseId);
|
||||
if (stepId !== undefined) {
|
||||
this.runningTools.delete(toolUseId);
|
||||
const step = this.steps.find((s) => s.id === stepId);
|
||||
if (step) {
|
||||
const isError = evt.error === true || evt.isError === true;
|
||||
step.status = isError ? "error" : "done";
|
||||
step.duration = Date.now() - step.timestamp;
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stepIcon(status: string): vscode.ThemeIcon {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return new vscode.ThemeIcon("sync~spin", new vscode.ThemeColor("charts.yellow"));
|
||||
case "done":
|
||||
return new vscode.ThemeIcon("pass", new vscode.ThemeColor("testing.iconPassed"));
|
||||
case "error":
|
||||
return new vscode.ThemeIcon("error", new vscode.ThemeColor("testing.iconFailed"));
|
||||
default:
|
||||
return new vscode.ThemeIcon("circle-outline");
|
||||
}
|
||||
}
|
||||
|
||||
function describeStep(toolName: string, input: Record<string, unknown>): string {
|
||||
switch (toolName) {
|
||||
case "Read": {
|
||||
const p = String(input.file_path ?? input.path ?? "");
|
||||
return `Read ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Write": {
|
||||
const p = String(input.file_path ?? "");
|
||||
return `Write ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Edit": {
|
||||
const p = String(input.file_path ?? "");
|
||||
return `Edit ${p.split(/[\\/]/).pop() ?? p}`;
|
||||
}
|
||||
case "Bash":
|
||||
return `$ ${String(input.command ?? "").slice(0, 50)}`;
|
||||
case "Grep":
|
||||
return `Grep: ${String(input.pattern ?? "").slice(0, 40)}`;
|
||||
case "Glob":
|
||||
return `Glob: ${String(input.pattern ?? "").slice(0, 40)}`;
|
||||
default:
|
||||
return toolName;
|
||||
}
|
||||
}
|
||||
124
vscode-extension/src/scm-provider.ts
Normal file
124
vscode-extension/src/scm-provider.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import * as vscode from "vscode";
|
||||
import * as path from "node:path";
|
||||
import type { GsdChangeTracker } from "./change-tracker.js";
|
||||
|
||||
const GSD_ORIGINAL_SCHEME = "gsd-original";
|
||||
|
||||
/**
|
||||
* Source Control provider that shows files modified by the GSD agent
|
||||
* in a dedicated "GSD Agent" section of the Source Control panel.
|
||||
* Supports QuickDiff to show before/after diffs, and accept/discard per-file.
|
||||
*/
|
||||
export class GsdScmProvider implements vscode.Disposable {
|
||||
private readonly scm: vscode.SourceControl;
|
||||
private readonly changesGroup: vscode.SourceControlResourceGroup;
|
||||
private readonly contentProvider: GsdOriginalContentProvider;
|
||||
private disposables: vscode.Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly tracker: GsdChangeTracker,
|
||||
private readonly workspaceRoot: string,
|
||||
) {
|
||||
// Register content provider for original file contents
|
||||
this.contentProvider = new GsdOriginalContentProvider(tracker);
|
||||
this.disposables.push(
|
||||
vscode.workspace.registerTextDocumentContentProvider(
|
||||
GSD_ORIGINAL_SCHEME,
|
||||
this.contentProvider,
|
||||
),
|
||||
);
|
||||
|
||||
// Create source control instance
|
||||
this.scm = vscode.scm.createSourceControl(
|
||||
"gsd",
|
||||
"GSD Agent",
|
||||
vscode.Uri.file(workspaceRoot),
|
||||
);
|
||||
this.scm.quickDiffProvider = {
|
||||
provideOriginalResource: (uri: vscode.Uri): vscode.Uri | undefined => {
|
||||
const filePath = uri.fsPath;
|
||||
if (this.tracker.getOriginal(filePath) !== undefined) {
|
||||
return uri.with({ scheme: GSD_ORIGINAL_SCHEME });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
this.scm.inputBox.placeholder = "Describe changes to accept...";
|
||||
this.scm.acceptInputCommand = {
|
||||
command: "gsd.acceptAllChanges",
|
||||
title: "Accept All",
|
||||
};
|
||||
this.scm.count = 0;
|
||||
this.disposables.push(this.scm);
|
||||
|
||||
// Create resource group
|
||||
this.changesGroup = this.scm.createResourceGroup("changes", "Agent Changes");
|
||||
this.changesGroup.hideWhenEmpty = true;
|
||||
this.disposables.push(this.changesGroup);
|
||||
|
||||
// Listen for change tracker updates
|
||||
this.disposables.push(
|
||||
tracker.onDidChange(() => this.refresh()),
|
||||
);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private refresh(): void {
|
||||
const files = this.tracker.modifiedFiles;
|
||||
this.changesGroup.resourceStates = files.map((filePath) => {
|
||||
const uri = vscode.Uri.file(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
const relativePath = path.relative(this.workspaceRoot, filePath);
|
||||
|
||||
const state: vscode.SourceControlResourceState = {
|
||||
resourceUri: uri,
|
||||
decorations: {
|
||||
strikeThrough: false,
|
||||
tooltip: `Modified by GSD Agent`,
|
||||
light: { iconPath: new vscode.ThemeIcon("edit") },
|
||||
dark: { iconPath: new vscode.ThemeIcon("edit") },
|
||||
},
|
||||
command: {
|
||||
command: "vscode.diff",
|
||||
title: "Show Changes",
|
||||
arguments: [
|
||||
uri.with({ scheme: GSD_ORIGINAL_SCHEME }),
|
||||
uri,
|
||||
`${fileName} (GSD Agent Changes)`,
|
||||
],
|
||||
},
|
||||
};
|
||||
return state;
|
||||
});
|
||||
this.scm.count = files.length;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const d of this.disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TextDocumentContentProvider that serves the original (pre-agent) content
|
||||
* of files via the `gsd-original:` URI scheme.
|
||||
*/
|
||||
class GsdOriginalContentProvider implements vscode.TextDocumentContentProvider {
|
||||
private readonly _onDidChange = new vscode.EventEmitter<vscode.Uri>();
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
constructor(private readonly tracker: GsdChangeTracker) {
|
||||
tracker.onDidChange((paths) => {
|
||||
for (const p of paths) {
|
||||
this._onDidChange.fire(vscode.Uri.file(p).with({ scheme: GSD_ORIGINAL_SCHEME }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
provideTextDocumentContent(uri: vscode.Uri): string {
|
||||
const filePath = uri.with({ scheme: "file" }).fsPath;
|
||||
return this.tracker.getOriginal(filePath) ?? "";
|
||||
}
|
||||
}
|
||||
|
|
@ -56,18 +56,35 @@ export class GsdSessionTreeProvider implements vscode.TreeDataProvider<SessionIt
|
|||
|
||||
const items: SessionItem[] = [];
|
||||
for (const file of files) {
|
||||
// Filename format: <unixTimestampMs>_<sessionId>.jsonl
|
||||
const match = file.match(/^(\d+)_(.+)\.jsonl$/);
|
||||
if (!match) {
|
||||
const sessionFile = path.join(sessionDir, file);
|
||||
|
||||
// Try two filename formats:
|
||||
// 1. ISO timestamp: 2026-03-23T17-49-05-784Z_<sessionId>.jsonl
|
||||
// 2. Unix timestamp: <unixTimestampMs>_<sessionId>.jsonl
|
||||
const isoMatch = file.match(/^(\d{4}-\d{2}-\d{2}T[\d-]+Z)_(.+)\.jsonl$/);
|
||||
const unixMatch = file.match(/^(\d{10,})_(.+)\.jsonl$/);
|
||||
|
||||
let timestamp: Date;
|
||||
let sessionId: string;
|
||||
|
||||
if (isoMatch) {
|
||||
// Convert ISO-like format (dashes instead of colons) back to parseable ISO
|
||||
const isoStr = isoMatch[1].replace(/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "$1:$2:$3.$4Z");
|
||||
timestamp = new Date(isoStr);
|
||||
sessionId = isoMatch[2];
|
||||
} else if (unixMatch) {
|
||||
timestamp = new Date(parseInt(unixMatch[1], 10));
|
||||
sessionId = unixMatch[2];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const ts = parseInt(match[1], 10);
|
||||
const sessionId = match[2];
|
||||
const sessionFile = path.join(sessionDir, file);
|
||||
|
||||
if (isNaN(timestamp.getTime())) continue;
|
||||
|
||||
items.push({
|
||||
label: formatDate(new Date(ts)),
|
||||
label: formatDate(timestamp),
|
||||
sessionFile,
|
||||
timestamp: new Date(ts),
|
||||
timestamp,
|
||||
sessionId,
|
||||
isCurrent: sessionFile === state.sessionFile,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,8 +2,17 @@ import * as vscode from "vscode";
|
|||
import type { GsdClient, SessionStats, ThinkingLevel } from "./gsd-client.js";
|
||||
|
||||
/**
|
||||
* WebviewViewProvider that renders a sidebar panel showing connection status,
|
||||
* model info, thinking level, token usage, cost, and quick action controls.
|
||||
* Send a message through VS Code's Chat panel so the user sees the response.
|
||||
* Opens the Chat panel and pre-fills the @gsd participant with the message.
|
||||
*/
|
||||
async function sendViaChat(message: string): Promise<void> {
|
||||
await vscode.commands.executeCommand("workbench.action.chat.open", { query: message });
|
||||
}
|
||||
|
||||
/**
|
||||
* WebviewViewProvider that renders a compact, card-based sidebar panel.
|
||||
* Designed for information density without clutter — collapsible sections,
|
||||
* hidden empty data, and consolidated action buttons.
|
||||
*/
|
||||
export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewId = "gsd-sidebar";
|
||||
|
|
@ -106,22 +115,18 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
await vscode.commands.executeCommand("gsd.copyLastResponse");
|
||||
break;
|
||||
case "autoMode":
|
||||
if (this.client.isConnected) {
|
||||
await this.client.sendPrompt("/gsd auto").catch(() => {});
|
||||
}
|
||||
await sendViaChat("@gsd /gsd auto");
|
||||
break;
|
||||
case "nextUnit":
|
||||
if (this.client.isConnected) {
|
||||
await this.client.sendPrompt("/gsd next").catch(() => {});
|
||||
}
|
||||
await sendViaChat("@gsd /gsd next");
|
||||
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(() => {});
|
||||
if (quickInput) {
|
||||
await sendViaChat(`@gsd /gsd quick ${quickInput}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -130,15 +135,13 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
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(() => {});
|
||||
if (thought) {
|
||||
await sendViaChat(`@gsd /gsd capture ${thought}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "status":
|
||||
if (this.client.isConnected) {
|
||||
await this.client.sendPrompt("/gsd status").catch(() => {});
|
||||
}
|
||||
await sendViaChat("@gsd /gsd status");
|
||||
break;
|
||||
case "forkSession":
|
||||
await vscode.commands.executeCommand("gsd.forkSession");
|
||||
|
|
@ -149,6 +152,9 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
case "toggleFollowUpMode":
|
||||
await vscode.commands.executeCommand("gsd.toggleFollowUpMode");
|
||||
break;
|
||||
case "showHistory":
|
||||
await vscode.commands.executeCommand("gsd.showHistory");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -168,6 +174,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
}
|
||||
|
||||
let modelName = "N/A";
|
||||
let modelShort = "";
|
||||
let sessionId = "N/A";
|
||||
let sessionName = "";
|
||||
let messageCount = 0;
|
||||
|
|
@ -189,6 +196,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
modelName = state.model
|
||||
? `${state.model.provider}/${state.model.id}`
|
||||
: "Not set";
|
||||
modelShort = state.model?.id ?? "";
|
||||
sessionId = state.sessionId;
|
||||
sessionName = state.sessionName ?? "";
|
||||
messageCount = state.messageCount;
|
||||
|
|
@ -216,6 +224,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
this.view.webview.html = this.getHtml({
|
||||
connected,
|
||||
modelName,
|
||||
modelShort,
|
||||
sessionId,
|
||||
sessionName,
|
||||
messageCount,
|
||||
|
|
@ -244,6 +253,7 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
private getHtml(info: {
|
||||
connected: boolean;
|
||||
modelName: string;
|
||||
modelShort: string;
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
messageCount: number;
|
||||
|
|
@ -259,57 +269,49 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
followUpMode: "all" | "one-at-a-time";
|
||||
}): string {
|
||||
const statusColor = info.connected ? "#4ec9b0" : "#f44747";
|
||||
const statusText = info.connected
|
||||
? info.isStreaming
|
||||
? "Processing..."
|
||||
: info.isCompacting
|
||||
? "Compacting..."
|
||||
: "Connected"
|
||||
: "Disconnected";
|
||||
const statusLabel = info.isStreaming ? "Working" : info.isCompacting ? "Compacting" : info.connected ? "Connected" : "Disconnected";
|
||||
|
||||
const inputTokens = info.stats?.inputTokens?.toLocaleString() ?? "-";
|
||||
const outputTokens = info.stats?.outputTokens?.toLocaleString() ?? "-";
|
||||
const cacheRead = info.stats?.cacheReadTokens?.toLocaleString() ?? "-";
|
||||
const cacheWrite = info.stats?.cacheWriteTokens?.toLocaleString() ?? "-";
|
||||
const turnCount = info.stats?.turnCount?.toString() ?? "-";
|
||||
const duration = info.stats?.duration !== undefined
|
||||
? `${Math.round(info.stats.duration / 1000)}s`
|
||||
: "-";
|
||||
const cost = info.stats?.totalCost !== undefined ? `$${info.stats.totalCost.toFixed(4)}` : "-";
|
||||
// Model short name for header
|
||||
const modelDisplay = info.modelShort || "N/A";
|
||||
|
||||
const thinkingBadge = info.thinkingLevel !== "off"
|
||||
? `<span class="badge">${info.thinkingLevel}</span>`
|
||||
: `<span class="badge muted">off</span>`;
|
||||
// Session display — name or truncated ID
|
||||
const sessionDisplay = info.sessionName || (info.sessionId !== "N/A" ? info.sessionId.slice(0, 8) : "N/A");
|
||||
|
||||
const autoCompBadge = info.autoCompaction
|
||||
? `<span class="badge">on</span>`
|
||||
: `<span class="badge muted">off</span>`;
|
||||
|
||||
const autoRetryBadge = info.autoRetry
|
||||
? `<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>`
|
||||
// Cost for header
|
||||
const costDisplay = info.stats?.totalCost !== undefined && info.stats.totalCost > 0
|
||||
? `$${info.stats.totalCost.toFixed(4)}`
|
||||
: "";
|
||||
|
||||
// Context window usage
|
||||
// Context window
|
||||
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"
|
||||
? `<span class="badge">1-at-a-time</span>`
|
||||
: `<span class="badge muted">all</span>`;
|
||||
const followUpBadge = info.followUpMode === "one-at-a-time"
|
||||
? `<span class="badge">1-at-a-time</span>`
|
||||
: `<span class="badge muted">all</span>`;
|
||||
// Only show stats that have real data
|
||||
const hasStats = info.stats && (
|
||||
(info.stats.inputTokens !== undefined && info.stats.inputTokens > 0) ||
|
||||
(info.stats.outputTokens !== undefined && info.stats.outputTokens > 0)
|
||||
);
|
||||
|
||||
const nonce = getNonce();
|
||||
|
||||
// Build stat rows only for non-zero values
|
||||
let statRows = "";
|
||||
if (hasStats && info.stats) {
|
||||
const pairs: [string, string][] = [];
|
||||
if (info.stats.inputTokens) pairs.push(["In", formatNum(info.stats.inputTokens)]);
|
||||
if (info.stats.outputTokens) pairs.push(["Out", formatNum(info.stats.outputTokens)]);
|
||||
if (info.stats.cacheReadTokens) pairs.push(["Cache R", formatNum(info.stats.cacheReadTokens)]);
|
||||
if (info.stats.cacheWriteTokens) pairs.push(["Cache W", formatNum(info.stats.cacheWriteTokens)]);
|
||||
if (info.stats.turnCount) pairs.push(["Turns", String(info.stats.turnCount)]);
|
||||
if (info.stats.duration) pairs.push(["Time", `${Math.round(info.stats.duration / 1000)}s`]);
|
||||
if (info.stats.totalCost !== undefined && info.stats.totalCost > 0) pairs.push(["Cost", `$${info.stats.totalCost.toFixed(4)}`]);
|
||||
|
||||
statRows = pairs.map(([k, v]) =>
|
||||
`<span class="stat-label">${k}</span><span class="stat-value">${v}</span>`
|
||||
).join("");
|
||||
}
|
||||
|
||||
return /* html */ `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
|
@ -317,291 +319,329 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
color: var(--vscode-foreground);
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
.status-row {
|
||||
|
||||
/* ---- Header card ---- */
|
||||
.header {
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vscode-editor-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.header-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: ${statusColor};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.streaming-indicator {
|
||||
.status-label {
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-model {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.header-model:hover { opacity: 1; }
|
||||
.header-cost {
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.header-sub .sep { opacity: 0.3; }
|
||||
.session-name {
|
||||
cursor: pointer;
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.session-name:hover { opacity: 1; text-decoration: underline; }
|
||||
|
||||
/* ---- Streaming banner ---- */
|
||||
.streaming {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--vscode-editor-background);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
background: color-mix(in srgb, var(--vscode-focusBorder) 15%, transparent);
|
||||
border: 1px solid var(--vscode-focusBorder);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid var(--vscode-foreground);
|
||||
width: 10px; height: 10px;
|
||||
border: 2px solid var(--vscode-focusBorder);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@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 {
|
||||
width: 100%;
|
||||
}
|
||||
.info-table td {
|
||||
padding: 3px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.info-table td:first-child {
|
||||
opacity: 0.7;
|
||||
padding-right: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.info-table td:last-child {
|
||||
word-break: break-all;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.streaming-abort {
|
||||
margin-left: auto;
|
||||
font-size: 10px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--vscode-foreground);
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
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;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.badge.clickable:hover {
|
||||
opacity: 0.8;
|
||||
.streaming-abort:hover { opacity: 1; }
|
||||
|
||||
/* ---- Context bar (inline in header) ---- */
|
||||
.context-bar {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.btn-row button {
|
||||
flex: 1;
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
.context-track {
|
||||
width: 100%;
|
||||
padding: 6px 14px;
|
||||
border: none;
|
||||
height: 3px;
|
||||
background: var(--vscode-panel-border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.context-fill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.context-text {
|
||||
font-size: 10px;
|
||||
opacity: 0.5;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* ---- Collapsible section ---- */
|
||||
.section {
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
font-size: var(--vscode-font-size);
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
}
|
||||
button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
button.secondary {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
}
|
||||
button.secondary:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
.token-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.token-stats .label {
|
||||
user-select: none;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
opacity: 0.7;
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
.token-stats .value {
|
||||
.section-header:hover { opacity: 1; }
|
||||
.chevron {
|
||||
font-size: 10px;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.section.collapsed .section-body { display: none; }
|
||||
.section.collapsed .chevron { transform: rotate(-90deg); }
|
||||
.section-body {
|
||||
padding: 6px 10px 8px;
|
||||
}
|
||||
|
||||
/* ---- Stats grid ---- */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 2px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.stat-label { opacity: 0.6; }
|
||||
.stat-value {
|
||||
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 {
|
||||
|
||||
/* ---- Toggle row ---- */
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 3px 0;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.toggle-label { opacity: 0.7; }
|
||||
.toggle-pill {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.toggle-pill.on {
|
||||
background: color-mix(in srgb, var(--vscode-focusBorder) 30%, transparent);
|
||||
border-color: var(--vscode-focusBorder);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
.toggle-pill.off {
|
||||
background: transparent;
|
||||
border-color: var(--vscode-panel-border);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.toggle-pill:hover { opacity: 1; }
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.actions.three-col {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 5px 6px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
.action-btn:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
.action-btn.primary {
|
||||
background: var(--vscode-button-background);
|
||||
color: var(--vscode-button-foreground);
|
||||
border-color: var(--vscode-button-background);
|
||||
font-weight: 600;
|
||||
}
|
||||
.action-btn.primary:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
.action-btn.danger {
|
||||
border-color: #f44747;
|
||||
color: #f44747;
|
||||
}
|
||||
.action-btn.danger:hover {
|
||||
background: color-mix(in srgb, #f44747 15%, transparent);
|
||||
}
|
||||
.action-btn.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* ---- Disconnected state ---- */
|
||||
.disconnected {
|
||||
text-align: center;
|
||||
padding: 20px 12px;
|
||||
}
|
||||
.disconnected p {
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.start-btn {
|
||||
padding: 8px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: var(--vscode-font-size);
|
||||
font-weight: 600;
|
||||
color: var(--vscode-button-foreground);
|
||||
background: var(--vscode-button-background);
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
}
|
||||
.start-btn:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="status-row">
|
||||
<div class="status-dot"></div>
|
||||
<strong>${statusText}</strong>
|
||||
</div>
|
||||
|
||||
${streamingIndicator}
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Session</div>
|
||||
<table class="info-table">
|
||||
<tr><td>Model</td><td>${escapeHtml(info.modelName)}</td></tr>
|
||||
<tr>
|
||||
<td>Session</td>
|
||||
<td>
|
||||
${escapeHtml(info.sessionName || info.sessionId)}
|
||||
${info.connected ? `<span class="badge clickable" data-command="setSessionName" title="Rename session" style="margin-left:4px">✎</span>` : ""}
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>Messages</td><td>${info.messageCount}${info.pendingMessageCount > 0 ? ` <span class="badge muted">+${info.pendingMessageCount} pending</span>` : ""}</td></tr>
|
||||
<tr>
|
||||
<td>Thinking</td>
|
||||
<td>${thinkingBadge}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Auto-compact</td>
|
||||
<td>${autoCompBadge}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Auto-retry</td>
|
||||
<td>${autoRetryBadge}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Steering</td>
|
||||
<td><span class="badge clickable" data-command="toggleSteeringMode">${info.steeringMode === "one-at-a-time" ? "1-at-a-time" : "all"}</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Follow-up</td>
|
||||
<td><span class="badge clickable" data-command="toggleFollowUpMode">${info.followUpMode === "one-at-a-time" ? "1-at-a-time" : "all"}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</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">Cache read</span>
|
||||
<span class="value">${cacheRead}</span>
|
||||
<span class="label">Cache write</span>
|
||||
<span class="value">${cacheWrite}</span>
|
||||
<span class="label">Turns</span>
|
||||
<span class="value">${turnCount}</span>
|
||||
<span class="label">Duration</span>
|
||||
<span class="value">${duration}</span>
|
||||
<span class="label">Cost</span>
|
||||
<span class="value">${cost}</span>
|
||||
${info.connected ? this.getConnectedHtml(info, {
|
||||
statusLabel,
|
||||
modelDisplay,
|
||||
sessionDisplay,
|
||||
costDisplay,
|
||||
contextPct,
|
||||
contextColor,
|
||||
hasStats: !!hasStats,
|
||||
statRows,
|
||||
nonce,
|
||||
}) : `
|
||||
<div class="header">
|
||||
<div class="header-top">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-label">Disconnected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${info.contextWindow > 0 ? `
|
||||
<div class="section">
|
||||
<div class="section-title">Context Window</div>
|
||||
<div class="context-bar-outer">
|
||||
<div class="context-bar-inner" style="width: ${contextPct}%; background: ${contextColor};"></div>
|
||||
</div>
|
||||
<div class="context-label">${contextLabel}</div>
|
||||
<div class="disconnected">
|
||||
<p>Agent is not running</p>
|
||||
<button class="start-btn" data-command="start">Start Agent</button>
|
||||
</div>
|
||||
` : ""}
|
||||
` : ""}
|
||||
|
||||
${info.connected ? `
|
||||
<div class="section">
|
||||
<div class="section-title">Workflow</div>
|
||||
<div class="btn-group">
|
||||
<div class="btn-row">
|
||||
<button data-command="autoMode">Auto</button>
|
||||
<button class="secondary" data-command="nextUnit">Next</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="quickTask">Quick</button>
|
||||
<button class="secondary" data-command="capture">Capture</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="status">Status</button>
|
||||
<button class="secondary" data-command="forkSession">Fork</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Controls</div>
|
||||
<div class="btn-group">
|
||||
${info.connected
|
||||
? `<button data-command="stop">Stop Agent</button>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="newSession">New Session</button>
|
||||
<button class="secondary" data-command="switchModel">Model</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="cycleThinking">Thinking</button>
|
||||
<button class="secondary" data-command="toggleAutoCompaction">Auto-Compact</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="toggleAutoRetry">Auto-Retry</button>
|
||||
<button class="secondary" data-command="copyLastResponse">Copy Response</button>
|
||||
</div>`
|
||||
: `<button data-command="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" data-command="compact">Compact</button>
|
||||
<button class="secondary" data-command="exportHtml">Export</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="secondary" data-command="abort">Abort</button>
|
||||
<button class="secondary" data-command="listCommands">Commands</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
`}
|
||||
|
||||
<script nonce="${nonce}">
|
||||
const vscode = acquireVsCodeApi();
|
||||
const stored = vscode.getState() || {};
|
||||
|
||||
// Restore collapsed state
|
||||
document.querySelectorAll('.section').forEach(s => {
|
||||
const id = s.dataset.section;
|
||||
if (id && stored[id] === 'collapsed') s.classList.add('collapsed');
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
// Section toggle
|
||||
const header = e.target.closest('.section-header');
|
||||
if (header) {
|
||||
const section = header.parentElement;
|
||||
section.classList.toggle('collapsed');
|
||||
const id = section.dataset.section;
|
||||
if (id) {
|
||||
const state = vscode.getState() || {};
|
||||
state[id] = section.classList.contains('collapsed') ? 'collapsed' : 'open';
|
||||
vscode.setState(state);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Button/command click
|
||||
const btn = e.target.closest('[data-command]');
|
||||
if (btn) {
|
||||
vscode.postMessage({ command: btn.dataset.command });
|
||||
|
|
@ -611,6 +651,144 @@ export class GsdSidebarProvider implements vscode.WebviewViewProvider {
|
|||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private getConnectedHtml(
|
||||
info: {
|
||||
connected: boolean;
|
||||
modelName: string;
|
||||
modelShort: string;
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
messageCount: number;
|
||||
pendingMessageCount: number;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
isStreaming: boolean;
|
||||
isCompacting: boolean;
|
||||
autoCompaction: boolean;
|
||||
autoRetry: boolean;
|
||||
stats: SessionStats | null;
|
||||
contextWindow: number;
|
||||
steeringMode: "all" | "one-at-a-time";
|
||||
followUpMode: "all" | "one-at-a-time";
|
||||
},
|
||||
ui: {
|
||||
statusLabel: string;
|
||||
modelDisplay: string;
|
||||
sessionDisplay: string;
|
||||
costDisplay: string;
|
||||
contextPct: number;
|
||||
contextColor: string;
|
||||
hasStats: boolean;
|
||||
statRows: string;
|
||||
nonce: string;
|
||||
},
|
||||
): string {
|
||||
const pendingBadge = info.pendingMessageCount > 0
|
||||
? ` <span style="opacity:0.5">+${info.pendingMessageCount}</span>`
|
||||
: "";
|
||||
|
||||
return `
|
||||
<!-- Header card -->
|
||||
<div class="header">
|
||||
<div class="header-top">
|
||||
<div class="status-dot"></div>
|
||||
<span class="status-label">${ui.statusLabel}</span>
|
||||
<span class="header-model" data-command="switchModel" title="${escapeHtml(info.modelName)}">${escapeHtml(ui.modelDisplay)}</span>
|
||||
${ui.costDisplay ? `<span class="header-cost">${ui.costDisplay}</span>` : ""}
|
||||
</div>
|
||||
<div class="header-sub">
|
||||
<span class="session-name" data-command="setSessionName" title="${escapeHtml(info.sessionId)}">${escapeHtml(ui.sessionDisplay)}</span>
|
||||
<span class="sep">/</span>
|
||||
<span>${info.messageCount} msg${pendingBadge}</span>
|
||||
<span class="sep">/</span>
|
||||
<span data-command="cycleThinking" style="cursor:pointer" title="Click to cycle thinking level">${info.thinkingLevel === "off" ? "no think" : info.thinkingLevel}</span>
|
||||
</div>
|
||||
${info.contextWindow > 0 ? `
|
||||
<div class="context-bar">
|
||||
<div class="context-track">
|
||||
<div class="context-fill" style="width:${ui.contextPct}%;background:${ui.contextColor}"></div>
|
||||
</div>
|
||||
<div class="context-text">${ui.contextPct}% context (${formatNum((info.stats?.inputTokens ?? 0) + (info.stats?.outputTokens ?? 0))} / ${formatNum(info.contextWindow)})</div>
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
|
||||
${info.isStreaming ? `
|
||||
<div class="streaming">
|
||||
<span class="spinner"></span>
|
||||
<span>Agent is working...</span>
|
||||
<button class="streaming-abort" data-command="abort">Stop</button>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<!-- Workflow -->
|
||||
<div class="section" data-section="workflow">
|
||||
<div class="section-header"><span class="chevron">▼</span> Workflow</div>
|
||||
<div class="section-body">
|
||||
<div class="actions">
|
||||
<button class="action-btn primary" data-command="autoMode">Auto</button>
|
||||
<button class="action-btn" data-command="nextUnit">Next</button>
|
||||
<button class="action-btn" data-command="quickTask">Quick</button>
|
||||
<button class="action-btn" data-command="capture">Capture</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${ui.hasStats ? `
|
||||
<!-- Stats -->
|
||||
<div class="section" data-section="stats">
|
||||
<div class="section-header"><span class="chevron">▼</span> Stats</div>
|
||||
<div class="section-body">
|
||||
<div class="stats-grid">${ui.statRows}</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="section" data-section="actions">
|
||||
<div class="section-header"><span class="chevron">▼</span> Actions</div>
|
||||
<div class="section-body">
|
||||
<div class="actions three-col">
|
||||
<button class="action-btn" data-command="newSession">New</button>
|
||||
<button class="action-btn" data-command="compact">Compact</button>
|
||||
<button class="action-btn" data-command="copyLastResponse">Copy</button>
|
||||
<button class="action-btn" data-command="status">Status</button>
|
||||
<button class="action-btn" data-command="fixProblemsInFile">Fix Errs</button>
|
||||
<button class="action-btn" data-command="showHistory">History</button>
|
||||
</div>
|
||||
<div style="margin-top:6px">
|
||||
<button class="action-btn danger full" data-command="stop">Stop Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings (collapsed by default) -->
|
||||
<div class="section collapsed" data-section="settings">
|
||||
<div class="section-header"><span class="chevron">▼</span> Settings</div>
|
||||
<div class="section-body">
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Auto-compact</span>
|
||||
<span class="toggle-pill ${info.autoCompaction ? "on" : "off"}" data-command="toggleAutoCompaction">${info.autoCompaction ? "on" : "off"}</span>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Auto-retry</span>
|
||||
<span class="toggle-pill ${info.autoRetry ? "on" : "off"}" data-command="toggleAutoRetry">${info.autoRetry ? "on" : "off"}</span>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Steering</span>
|
||||
<span class="toggle-pill ${info.steeringMode === "one-at-a-time" ? "on" : "off"}" data-command="toggleSteeringMode">${info.steeringMode === "one-at-a-time" ? "1-at-a-time" : "all"}</span>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Follow-up</span>
|
||||
<span class="toggle-pill ${info.followUpMode === "one-at-a-time" ? "on" : "off"}" data-command="toggleFollowUpMode">${info.followUpMode === "one-at-a-time" ? "1-at-a-time" : "all"}</span>
|
||||
</div>
|
||||
<div class="toggle-row">
|
||||
<span class="toggle-label">Approval</span>
|
||||
<span class="toggle-pill on" data-command="selectApprovalMode">change</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
|
|
@ -621,6 +799,12 @@ function escapeHtml(text: string): string {
|
|||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function formatNum(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function getNonce(): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let nonce = "";
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ export class GsdSlashCompletionProvider
|
|||
|
||||
private async refreshCache(): Promise<void> {
|
||||
try {
|
||||
this.cachedCommands = await this.client.getCommands();
|
||||
const all = await this.client.getCommands();
|
||||
// Only show /gsd commands — filter out unrelated extension/skill commands
|
||||
this.cachedCommands = all.filter((cmd) => cmd.name.startsWith("gsd"));
|
||||
} catch {
|
||||
// Silently ignore — agent may not be ready yet.
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue