Skip to content
71 changes: 69 additions & 2 deletions apps/desktop/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `extensions/` changes: `npm run gulp compile-extensions`
- `build/` changes: `cd build && npm run typecheck`
- Layering violations: `npm run valid-layers-check`
- Cyclic imports: `npm run check-cyclic-dependencies`
- Class field init order: `npm run define-class-fields-check`
- Monaco API surface: `npm run monaco-compile-check`
- TS security compliance: `npm run tsec-compile-check`
- vscode.d.ts / proposed APIs: `npm run vscode-dts-compile-check`

### Linting
- `npm run eslint` — TypeScript ESLint (flat config in `eslint.config.js`)
- `npm run stylelint` — CSS linting
- `npm run hygiene` — formatting and code quality checks
- `npm run precommit` — full pre-commit validation suite

### Running VS Code from source
- `./scripts/code.sh` (macOS/Linux) or `scripts\code.bat` (Windows) — launches Electron with the dev build
Expand Down Expand Up @@ -48,7 +59,7 @@ VS Code uses a strict **layered architecture**: `base` → `platform` → `edito
### Key patterns
- **Dependency injection**: Services are injected via constructor parameters (decorated with `@I*Service`). Non-service parameters must come after service parameters.
- **Contribution model**: Features register via `registerWorkbenchContribution2()` and contribute to extension points. Each contribution in `workbench/contrib/` is a self-contained feature module.
- **Platform targets**: Code is organized by runtime environment (`common/` = all, `browser/` = web, `node/` = Node.js, `electron-browser/` = Electron renderer, `electron-main/` = Electron main).
- **Platform targets**: Code is organized by runtime environment (`common/` = all, `browser/` = web, `node/` = Node.js, `electron-browser/` = Electron renderer, `electron-main/` = Electron main, `electron-utility/` = Electron utility process).
- **Disposables**: All event listeners and resources must be disposed. Use `DisposableStore`, `MutableDisposable`, or `DisposableMap`. Never register disposables to a class from a repeatedly-called method; return `IDisposable` instead.
- **Events vs method calls**: Events are for broadcasting state changes. Use direct method calls or service interactions for control flow between components.

Expand All @@ -74,7 +85,13 @@ VS Code uses a strict **layered architecture**: `base` → `platform` → `edito
- UI labels use title-style capitalization (prepositions of 4 or fewer letters are lowercase unless first/last)

### Code quality rules
- All files must include the Microsoft copyright header
- All files must include the Microsoft copyright header:
```
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
```
- Prefer `async`/`await` over `.then()` chains
- Do not use `any` or `unknown` without strong justification — define proper types
- Do not export types/functions unless shared across components
Expand Down Expand Up @@ -149,6 +166,56 @@ The app tracks Claude Code lifecycle events in each worktree via a hook-based no
### Environment variable
Terminals created in worktrees get `WORKSTREAMS_WORKTREE_PATH` injected so hook scripts can identify which worktree a Claude session belongs to.

## Workstream Review Comments

Inline review comments on diff editors that can be sent to Claude for automated fixes. Comments are scoped per worktree and stored in CLI-compatible JSON.

### Key files
- **Service interface**: `src/vs/workbench/services/workstreamComments/common/workstreamCommentService.ts` — `IWorkstreamCommentService`, `IWorkstreamComment`, `IWorkstreamCommentThread`
- **Service implementation**: `src/vs/workbench/services/workstreamComments/browser/workstreamCommentServiceImpl.ts` — persistence to `.workstreams/comments/{workstream}.json`
- **Comment controller**: `src/vs/workbench/contrib/workstreamComments/browser/workstreamCommentController.ts` — implements `ICommentController`, registers the "+" glyph on diff editors
- **Zone widget**: `src/vs/workbench/contrib/workstreamComments/browser/workstreamCommentZoneWidget.ts` — inline comment UI (edit/display modes)
- **Contribution + send action**: `src/vs/workbench/contrib/workstreamComments/browser/workstreamComments.contribution.ts` — startup registration and "Send Review Comments to Claude" command

### How commenting works
1. User opens a diff editor (must be split-side mode — inline diff not supported)
2. Clicking the "+" gutter glyph calls `CommentController.createCommentThreadTemplate()`
3. A `WorkstreamCommentZoneWidget` opens in edit mode with a textarea
4. On submit (Ctrl+Enter or click Comment), the service writes the comment to disk at `{repoPath}/.workstreams/comments/{worktreeName}.json`
5. Widget switches to display mode showing the saved comment with Edit/Delete buttons

### How sending comments to Claude works
Command: **"Workstream: Send Review Comments to Claude"** (Command Palette, id: `workstreamComments.sendToClaude`)

1. Fetches all comments for the active worktree via `commentService.getComments(worktree.name)`
2. Opens a QuickPick listing each comment with file:line, side (original/modified), and preview — all pre-selected
3. User can deselect individual comments or click "Send All"
4. Creates a new terminal, runs `claude`, waits 2 seconds for initialization
5. Sends a formatted markdown prompt listing each comment with file path, line number, side, and text
6. Deletes all sent comments from disk and memory
7. Shows notification: "Sent N comment(s) to Claude and cleared them"

### Comment data model
```typescript
type CommentSide = 'old' | 'new';
type DiffLineType = 'add' | 'remove' | 'context';

IWorkstreamComment {
id: string; // UUID
filePath: string; // relative path within worktree
line: number;
side: CommentSide;
lineType?: DiffLineType;
lineContent?: string;
text: string;
createdAt: string; // ISO 8601
resolved: boolean;
}
```

### Storage
Comments persist to `{repoPath}/.workstreams/comments/{worktreeName}.json` in CLI-compatible format. The base path is set from the first orchestrator repository and updates on worktree/repo changes.

## Sessions Layer (Agent Window)

`src/vs/sessions/` is a complete, independent workbench implementation optimized for agent session workflows. It is **not** an extension of the standard workbench — it's a parallel window type with its own layout, parts, and contributions.
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,32 @@ See our [wiki](https://github.com/microsoft/vscode/wiki/Feedback-Channels) for a

Many of the core components and extensions to VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) repositories are separate from each other. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).

## Workstream Review Comments

This fork adds inline review comments on diff editors that can be sent directly to Claude for automated fixes.

### Adding comments

1. Open a diff editor for any worktree file (the editor must be in **split-side** mode — inline diff is not supported)
2. Hover over a line in the diff gutter and click the **"+"** button that appears
3. Type your comment in the textarea that opens inline
4. Press **Ctrl+Enter** (or click **Comment**) to save

Saved comments appear inline with **Edit** and **Delete** buttons. Comments are scoped to the active worktree and persist across editor restarts.

### Sending comments to Claude

1. Open the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`)
2. Run **"Workstream: Send Review Comments to Claude"**
3. A picker shows all comments for the active worktree, pre-selected — deselect any you want to keep, or click **Send All**
4. A new terminal opens, launches `claude`, and sends a formatted prompt with each comment's file path, line number, side (original/modified), and text
5. Claude processes the comments and applies fixes in the worktree
6. Sent comments are automatically deleted after transmission

### Where comments are stored

Comments persist as JSON at `{repoPath}/.workstreams/comments/{worktreeName}.json`, compatible with the CLI's comment format.

## Bundled Extensions

VS Code includes a set of built-in extensions located in the [extensions](extensions) folder, including grammars and snippets for many languages. Extensions that provide rich language support (inline suggestions, Go to Definition) for a language have the suffix `language-features`. For example, the `json` extension provides coloring for `JSON` and the `json-language-features` extension provides rich language support for `JSON`.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/vs/editor/common/config/diffEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ export const diffEditorDefaultOptions = {
isInEmbeddedEditor: false,
onlyShowAccessibleDiffViewer: false,
renderSideBySideInlineBreakpoint: 900,
useInlineViewWhenSpaceIsLimited: true,
useInlineViewWhenSpaceIsLimited: false,
compactMode: false,
} satisfies ValidDiffEditorBaseOptions;
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
SPLIT_EDITOR, TOGGLE_MAXIMIZE_EDITOR_GROUP, MOVE_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_INTO_NEW_WINDOW_COMMAND_ID, MOVE_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID, COPY_EDITOR_GROUP_INTO_NEW_WINDOW_COMMAND_ID,
NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID, MOVE_EDITOR_INTO_RIGHT_GROUP, MOVE_EDITOR_INTO_LEFT_GROUP, MOVE_EDITOR_INTO_ABOVE_GROUP, MOVE_EDITOR_INTO_BELOW_GROUP
} from './editorCommands.js';
import { GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, TOGGLE_DIFF_SIDE_BY_SIDE, DIFF_SWAP_SIDES } from './diffEditorCommands.js';
import { GOTO_NEXT_CHANGE, GOTO_PREVIOUS_CHANGE, TOGGLE_DIFF_IGNORE_TRIM_WHITESPACE, DIFF_SWAP_SIDES } from './diffEditorCommands.js';
import { inQuickPickContext, getQuickNavigateHandler } from '../../quickaccess.js';
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr, ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js';
Expand Down Expand Up @@ -419,7 +419,8 @@ MenuRegistry.appendMenuItem(MenuId.EditorSplitMoveSubmenu, { command: { id: SPLI
MenuRegistry.appendMenuItem(MenuId.EditorSplitMoveSubmenu, { command: { id: JOIN_EDITOR_IN_GROUP, title: localize('joinInGroup', "Join in Group"), precondition: MultipleEditorsSelectedInGroupContext.negate() }, group: '3_split_in_group', order: 10, when: SideBySideEditorActiveContext });

// Editor Title Menu
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') });
// Inline View toggle removed — comments only work in split diff mode
// MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: TOGGLE_DIFF_SIDE_BY_SIDE, title: localize('inlineView', "Inline View"), toggled: ContextKeyExpr.equals('config.diffEditor.renderSideBySide', false) }, group: '1_diff', order: 10, when: ContextKeyExpr.has('isInDiffEditor') });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: SHOW_EDITORS_IN_GROUP, title: localize('showOpenedEditors', "Show Opened Editors") }, group: '3_open', order: 10 });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_EDITORS_IN_GROUP_COMMAND_ID, title: localize('closeAll', "Close All") }, group: '5_close', order: 10 });
MenuRegistry.appendMenuItem(MenuId.EditorTitle, { command: { id: CLOSE_SAVED_EDITORS_COMMAND_ID, title: localize('closeAllSaved', "Close Saved") }, group: '5_close', order: 20 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,16 @@
opacity: 0.9;
}

.monaco-workbench .part.orchestrator .worktree-icon.state-waiting {
color: var(--vscode-editorWarning-foreground);
.monaco-workbench .part.orchestrator .worktree-icon.state-waiting.codicon {
background-color: var(--vscode-editorWarning-foreground);
color: var(--vscode-editor-background);
border-radius: 3px;
font: normal normal normal 10px/1 codicon;
width: 14px;
height: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { IEditorGroupsService, IEditorWorkingSet } from '../../../services/edito
import { ILogService } from '../../../../platform/log/common/log.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';

interface IPersistedRepositoryState {
readonly path: string;
Expand Down Expand Up @@ -84,6 +85,7 @@ export class OrchestratorServiceImpl extends Disposable implements IOrchestrator
@ILogService private readonly logService: ILogService,
@INotificationService private readonly notificationService: INotificationService,
@IStorageService private readonly storageService: IStorageService,
@IProgressService private readonly progressService: IProgressService,
) {
super();
this._part = this._register(instantiationService.createInstance(OrchestratorPart));
Expand Down Expand Up @@ -301,25 +303,44 @@ export class OrchestratorServiceImpl extends Disposable implements IOrchestrator
}

/**
* Step 3: Restore editor state for target worktree (or clean slate).
* Step 3: Clear editors to a blank slate. Don't restore the
* target working set yet — diff editors would fail because the
* git extension still has the old worktree's repo.
*/
const savedSet = this._workingSetMap.get(worktree.path);
await this.editorGroupsService.applyWorkingSet(savedSet ?? 'empty');
await this.editorGroupsService.applyWorkingSet('empty');

/**
* Step 4: Swap workspace folder.
* Steps 4-5: Swap workspace folder and restore editors.
* Wrapped in a progress indicator so the user sees a loading
* state instead of a blank editor area.
*/
const folderData = { uri: URI.file(worktree.path) };
const currentFolders = this.workspaceContextService.getWorkspace().folders;
if (currentFolders.length === 0) {
await this.workspaceEditingService.addFolders([folderData], true);
} else {
await this.workspaceEditingService.updateFolders(0, currentFolders.length, [folderData], true);
}
const savedSet = this._workingSetMap.get(worktree.path);
await this.progressService.withProgress(
{
location: ProgressLocation.Window,
title: localize('switchingWorktree', "Switching to {0}...", worktree.name),
},
async () => {
// Step 4: Swap workspace folder — ext host restarts
const folderData = { uri: URI.file(worktree.path) };
const currentFolders = this.workspaceContextService.getWorkspace().folders;
if (currentFolders.length === 0) {
await this.workspaceEditingService.addFolders([folderData], true);
} else {
await this.workspaceEditingService.updateFolders(0, currentFolders.length, [folderData], true);
}

// Step 5: Wait for ext host to settle, then restore editors
if (savedSet) {
await new Promise(resolve => setTimeout(resolve, 1500));
await this.editorGroupsService.applyWorkingSet(savedSet);
}
},
);

/**
* Step 5: Fire after working set + folder swap — listeners show
* terminals for the new worktree. No race with save/apply possible.
* Step 6: Fire after folder swap — listeners show terminals for
* the new worktree.
*/
this._onDidApplyWorktreeEditorState.fire(worktree);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

.workstream-comment-widget {
padding: 8px 16px 8px 60px;
display: flex;
flex-direction: column;
gap: 8px;
background: var(--vscode-editor-background);
}

.workstream-comment-widget .ws-comment-header {
font-size: 12px;
color: var(--vscode-descriptionForeground);
font-weight: 600;
}

.workstream-comment-widget .ws-comment-body {
padding: 6px 0;
color: var(--vscode-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size, 13px);
white-space: pre-wrap;
word-wrap: break-word;
line-height: 1.4;
}

.workstream-comment-widget .ws-comment-textarea {
width: 100%;
min-height: 80px;
padding: 8px;
border: 1px solid var(--vscode-input-border, rgba(255, 255, 255, 0.1));
border-radius: 4px;
background: var(--vscode-input-background);
color: var(--vscode-input-foreground);
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size, 13px);
resize: vertical;
outline: none;
box-sizing: border-box;
}

.workstream-comment-widget .ws-comment-textarea:focus {
border-color: var(--vscode-focusBorder);
}

.workstream-comment-widget .ws-comment-textarea::placeholder {
color: var(--vscode-input-placeholderForeground);
}

.workstream-comment-widget .ws-comment-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}

.workstream-comment-widget .ws-comment-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-family: var(--vscode-font-family);
cursor: pointer;
outline: none;
}

.workstream-comment-widget .ws-comment-btn:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 1px;
}

.workstream-comment-widget .ws-comment-btn-cancel {
background: transparent;
color: var(--vscode-foreground);
border: 1px solid var(--vscode-input-border, rgba(255, 255, 255, 0.1));
}

.workstream-comment-widget .ws-comment-btn-cancel:hover {
background: var(--vscode-toolbar-hoverBackground);
}

.workstream-comment-widget .ws-comment-btn-submit {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
}

.workstream-comment-widget .ws-comment-btn-submit:hover {
background: var(--vscode-button-hoverBackground);
}

.workstream-comment-widget .ws-comment-btn-submit:disabled {
opacity: 0.5;
cursor: default;
}

.workstream-comment-widget .ws-comment-btn-delete {
background: transparent;
color: var(--vscode-errorForeground);
border: 1px solid transparent;
}

.workstream-comment-widget .ws-comment-btn-delete:hover:not(:disabled) {
border-color: var(--vscode-errorForeground);
}

.workstream-comment-widget .ws-comment-btn-delete:disabled {
opacity: 0.3;
cursor: default;
}
Loading