From 51ac4126aabe1ccf27242f6e0af5c29175a7b83c Mon Sep 17 00:00:00 2001 From: Dave Tseng Date: Fri, 7 Aug 2026 15:43:20 +0800 Subject: [PATCH 1/3] feat: add "Claude Actions" editor context menu (Explain / Add to Chat) Right-click in the code editor for two commands that reuse the existing at_mentioned/attachment plumbing: Explain stages the selection as a text attachment with an instruction header, Add to Chat @-mentions the file with the selection's line range. Both insert-not-submit, matching every other at_mentioned use in this codebase. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 2 + .../Attachments/AttachmentService.cs | 2 +- src/ClaudeCodeVS/ClaudeCodeVsPackage.cs | 34 ++++++++++++++ src/ClaudeCodeVS/Editor/SelectionService.cs | 47 +++++++++++++++++++ src/ClaudeCodeVS/VSCommandTable.vsct | 41 ++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index e15e978..281c0b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -164,3 +164,5 @@ claude --version # record the known-good version - `new_file_contents` in `openDiff` is in-memory -> write to a temp file to feed the comparison; write to `new_file_path` only on Accept. - Debounce `selection_changed` (~100–200ms) or you'll flood the socket. - **HTTP responses to the PowerShell shim/hooks MUST declare `charset=utf-8`** (and the shim additionally decodes raw response bytes as UTF-8 itself): PS 5.1's `Invoke-WebRequest` decodes charset-less responses as Latin-1, which mojibakes every non-ASCII character in every tool result (bit as `Microsoftâ„¢ Edge`, fixed 1.14.0). +- **VSCT: a `` can't be parented directly to another menu's ID** (e.g. `IDM_VS_CTXT_CODEWIN`) - it needs an intermediate `` parented to that ID, with the `` parented to the group (same pattern `ClaudeMenuGroup` uses for the Tools-menu button). Parenting the Menu straight to the menu ID compiles fine and the command still registers (`Commands.Item` finds it, `Commands.Raise` invokes it) but the submenu never renders in the actual context menu - silent, no error anywhere. Caught by literally screenshotting a right-click. +- **The Exp hive caches the compiled command table.** Editing `.vsct` and redeploying just the DLL isn't enough - new command IDs resolve via `Commands.Item`/`Raise` (proving the package loaded) but won't render in menus until you close devenv and run `devenv /rootsuffix Exp /updateconfiguration` once. Pure C#/XAML logic changes (no `.vsct` edit) don't need this - just close devenv, overwrite the hive's `ClaudeCodeVS.dll`, relaunch. diff --git a/src/ClaudeCodeVS/Attachments/AttachmentService.cs b/src/ClaudeCodeVS/Attachments/AttachmentService.cs index d20886a..6f7b7ca 100644 --- a/src/ClaudeCodeVS/Attachments/AttachmentService.cs +++ b/src/ClaudeCodeVS/Attachments/AttachmentService.cs @@ -364,7 +364,7 @@ private static string TokSuffix(long? est) => est is long t ? $" (≈{(t >= 1000 ? (t / 1000.0).ToString("0.0") + "k" : t.ToString())} tok)" : ""; /// Workspace-relative forward-slash form, or null when the path is outside the workspace. - private static string? ToWorkspaceRelative(string fullPath) + internal static string? ToWorkspaceRelative(string fullPath) { var ws = Ui.BridgeStatus.Workspace; if (string.IsNullOrEmpty(ws)) return null; diff --git a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs index f106a98..b5f8a76 100644 --- a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs +++ b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using ClaudeCodeVs.Protocol; using ClaudeCodeVs.Ui; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; @@ -46,6 +47,10 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke new CommandID(PackageGuids.CommandSet, PackageIds.LaunchClaude))); mcs.AddCommand(new MenuCommand(OnShowPanel, new CommandID(PackageGuids.CommandSet, PackageIds.ShowPanel))); + mcs.AddCommand(new MenuCommand(OnExplain, + new CommandID(PackageGuids.CommandSet, PackageIds.Explain))); + mcs.AddCommand(new MenuCommand(OnAddToChat, + new CommandID(PackageGuids.CommandSet, PackageIds.AddToChat))); } } @@ -64,6 +69,33 @@ private void OnShowPanel(object sender, EventArgs e) ErrorHandler.ThrowOnFailure(frame.Show()); } + // Editor right-click ("Claude Actions" submenu): Explain stages the selection as a text attachment + // with an instruction header (same insert-not-submit staging as a pasted prompt); Add to Chat just + // @-mentions the file/line-range in place. Both read SelectionService.Current, kept live by the + // MEF TextViewListener. + private void OnExplain(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + var sel = Editor.SelectionService.Current; + if (sel.IsEmpty) + { + Log.Warn("Explain: select some code first."); + return; + } + var header = sel.FilePath is null + ? "Explain this code:" + : $"Explain this code from {System.IO.Path.GetFileName(sel.FilePath)} (lines {sel.StartLine + 1}-{sel.EndLine + 1}):"; + JoinableTaskFactory.RunAsync(() => Attachments.AttachmentService.StageTextAsync(header + "\n\n" + sel.Text)) + .FileAndForget("claudecodevs/explain"); + } + + private void OnAddToChat(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + JoinableTaskFactory.RunAsync(() => Editor.SelectionService.MentionCurrentAsync()) + .FileAndForget("claudecodevs/addToChat"); + } + protected override void Dispose(bool disposing) { if (disposing) @@ -84,4 +116,6 @@ internal static class PackageIds // Must match the IDSymbol values in VSCommandTable.vsct. public const int LaunchClaude = 0x0100; public const int ShowPanel = 0x0101; + public const int Explain = 0x0102; + public const int AddToChat = 0x0103; } diff --git a/src/ClaudeCodeVS/Editor/SelectionService.cs b/src/ClaudeCodeVS/Editor/SelectionService.cs index 343ce71..61b8fb9 100644 --- a/src/ClaudeCodeVS/Editor/SelectionService.cs +++ b/src/ClaudeCodeVS/Editor/SelectionService.cs @@ -43,6 +43,53 @@ public static JToken LatestAsJson() lock (Gate) return (_lastNonEmpty ?? _current).ToJson(); } + /// The live selection snapshot, for the editor context-menu commands (Explain / Add to Chat). + public static SelectionInfo Current + { + get { lock (Gate) return _current; } + } + + /// + /// Push an at_mentioned for the current selection (file + line range), or the whole file if nothing + /// is selected - the "Add to Chat" context-menu command. Insert-not-submit, same as every other + /// at_mentioned in this codebase (CLAUDE.md). + /// + public static async Task MentionCurrentAsync() + { + var server = _server; + if (server is null || !server.HasConnections) + { + Log.Warn("Add to Chat: Claude isn't connected."); + return; + } + + var info = Current; + if (info.FilePath is null) + { + Log.Warn("Add to Chat: no active file."); + return; + } + + var mentionPath = Attachments.AttachmentService.ToWorkspaceRelative(info.FilePath) ?? info.FilePath; + var @params = new JObject { ["filePath"] = mentionPath }; + if (!info.IsEmpty) + { + @params["lineStart"] = info.StartLine; + @params["lineEnd"] = info.EndLine; + } + + try + { + await server.BroadcastNotificationAsync("at_mentioned", @params, CancellationToken.None); + var range = info.IsEmpty ? "" : $" (lines {info.StartLine + 1}-{info.EndLine + 1})"; + Log.Info($"Add to Chat: mentioned '{System.IO.Path.GetFileName(info.FilePath)}'{range}."); + } + catch (Exception e) + { + Log.Warn($"Add to Chat failed: {e.Message}"); + } + } + /// Record a fresh selection from a focused view and (debounced) push selection_changed. public static void Update(IWpfTextView view) { diff --git a/src/ClaudeCodeVS/VSCommandTable.vsct b/src/ClaudeCodeVS/VSCommandTable.vsct index ebb767c..7e221ab 100644 --- a/src/ClaudeCodeVS/VSCommandTable.vsct +++ b/src/ClaudeCodeVS/VSCommandTable.vsct @@ -13,8 +13,28 @@ + + + + + + + + + + + + + Claude Actions + Claude Actions + + + + + + @@ -43,8 +79,13 @@ + + + + + From 0d89bd451f541a3acb4959737bc971714c51dc43 Mon Sep 17 00:00:00 2001 From: Dave Tseng Date: Sat, 8 Aug 2026 11:32:34 +0800 Subject: [PATCH 2/3] fix: report an inclusive end line for Explain / Add to Chat SelectionInfo.EndLine is LSP-shaped (exclusive), so a whole-line selection parks the end at column 0 of the next line - the header and the at_mentioned range then claimed one line too many. Added EndLineInclusive and used it for both human-facing ranges and at_mentioned's lineEnd; the selection_changed / getCurrentSelection JSON keeps the exclusive LSP coordinates it's contractually required to send. Co-Authored-By: Claude Opus 5 --- src/ClaudeCodeVS/ClaudeCodeVsPackage.cs | 2 +- src/ClaudeCodeVS/Editor/SelectionService.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs index b5f8a76..daa0a51 100644 --- a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs +++ b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs @@ -84,7 +84,7 @@ private void OnExplain(object sender, EventArgs e) } var header = sel.FilePath is null ? "Explain this code:" - : $"Explain this code from {System.IO.Path.GetFileName(sel.FilePath)} (lines {sel.StartLine + 1}-{sel.EndLine + 1}):"; + : $"Explain this code from {System.IO.Path.GetFileName(sel.FilePath)} (lines {sel.StartLine + 1}-{sel.EndLineInclusive + 1}):"; JoinableTaskFactory.RunAsync(() => Attachments.AttachmentService.StageTextAsync(header + "\n\n" + sel.Text)) .FileAndForget("claudecodevs/explain"); } diff --git a/src/ClaudeCodeVS/Editor/SelectionService.cs b/src/ClaudeCodeVS/Editor/SelectionService.cs index 61b8fb9..6ad6212 100644 --- a/src/ClaudeCodeVS/Editor/SelectionService.cs +++ b/src/ClaudeCodeVS/Editor/SelectionService.cs @@ -75,13 +75,13 @@ public static async Task MentionCurrentAsync() if (!info.IsEmpty) { @params["lineStart"] = info.StartLine; - @params["lineEnd"] = info.EndLine; + @params["lineEnd"] = info.EndLineInclusive; } try { await server.BroadcastNotificationAsync("at_mentioned", @params, CancellationToken.None); - var range = info.IsEmpty ? "" : $" (lines {info.StartLine + 1}-{info.EndLine + 1})"; + var range = info.IsEmpty ? "" : $" (lines {info.StartLine + 1}-{info.EndLineInclusive + 1})"; Log.Info($"Add to Chat: mentioned '{System.IO.Path.GetFileName(info.FilePath)}'{range}."); } catch (Exception e) @@ -145,6 +145,14 @@ internal sealed class SelectionInfo public bool IsEmpty => Text.Length == 0; + /// + /// The last line the selection actually covers. is LSP-shaped (exclusive), so + /// selecting whole lines parks the end at column 0 of the NEXT line - reporting that one verbatim + /// would claim one line too many. Only for human-facing ranges and at_mentioned; the + /// selection_changed / getCurrentSelection JSON keeps the exclusive LSP coordinates. + /// + public int EndLineInclusive => EndChar == 0 && EndLine > StartLine ? EndLine - 1 : EndLine; + public SelectionInfo(string text, string? filePath, int startLine, int startChar, int endLine, int endChar) { Text = text; From 7259d79f96c89223544efecefd49778bb18dd937 Mon Sep 17 00:00:00 2001 From: Rishi Gulati Date: Tue, 11 Aug 2026 16:52:42 -0700 Subject: [PATCH 3/3] tweak: submenu named "Claude Code" (brand consistency) + mid-menu priority Two maintainer adjustments before merge: - "Claude Actions" -> "Claude Code": the menus now carry one brand (Tools > Launch Claude Code, View > Other Windows > Claude Code, and this flyout), matching the VS convention of the product name as the submenu (VS 2022's is just "Copilot"). - Context-menu group priority 0x0001 -> 0x0200: the code editor's right-click menu is the most muscle-memory-sensitive surface in VS, so the flyout sits below the navigation block rather than claiming the very top slot. Co-Authored-By: Claude Fable 5 --- src/ClaudeCodeVS/ClaudeCodeVsPackage.cs | 2 +- src/ClaudeCodeVS/VSCommandTable.vsct | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs index faa4f13..e05f22e 100644 --- a/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs +++ b/src/ClaudeCodeVS/ClaudeCodeVsPackage.cs @@ -100,7 +100,7 @@ private void OnShowPanel(object sender, EventArgs e) ErrorHandler.ThrowOnFailure(frame.Show()); } - // Editor right-click ("Claude Actions" submenu): Explain stages the selection as a text attachment + // Editor right-click ("Claude Code" submenu): Explain stages the selection as a text attachment // with an instruction header (same insert-not-submit staging as a pasted prompt); Add to Chat just // @-mentions the file/line-range in place. Both read SelectionService.Current, kept live by the // MEF TextViewListener. diff --git a/src/ClaudeCodeVS/VSCommandTable.vsct b/src/ClaudeCodeVS/VSCommandTable.vsct index 7e221ab..a5a712e 100644 --- a/src/ClaudeCodeVS/VSCommandTable.vsct +++ b/src/ClaudeCodeVS/VSCommandTable.vsct @@ -17,20 +17,23 @@ - + same as ClaudeMenuGroup does for the Tools menu placement above. MID priority on purpose: + the code editor's right-click menu is the most muscle-memory-sensitive surface in VS, so + the submenu sits below the navigation block (Go To Definition etc.), roughly where + Copilot's entries live - never at the very top. --> + - + - Claude Actions - Claude Actions + Claude Code + Claude Code