Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,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 `<Menu>` can't be parented directly to another menu's ID** (e.g. `IDM_VS_CTXT_CODEWIN`) - it needs an intermediate `<Group>` parented to that ID, with the `<Menu>` 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.
2 changes: 1 addition & 1 deletion src/ClaudeCodeVS/Attachments/AttachmentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)" : "";

/// <summary>Workspace-relative forward-slash form, or null when the path is outside the workspace.</summary>
private static string? ToWorkspaceRelative(string fullPath)
internal static string? ToWorkspaceRelative(string fullPath)
{
var ws = Ui.BridgeStatus.Workspace;
if (string.IsNullOrEmpty(ws)) return null;
Expand Down
34 changes: 34 additions & 0 deletions src/ClaudeCodeVS/ClaudeCodeVsPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,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)));
}
}

Expand Down Expand Up @@ -95,6 +100,33 @@ private void OnShowPanel(object sender, EventArgs e)
ErrorHandler.ThrowOnFailure(frame.Show());
}

// 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.
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.EndLineInclusive + 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)
Expand All @@ -115,4 +147,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;
}
55 changes: 55 additions & 0 deletions src/ClaudeCodeVS/Editor/SelectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,53 @@ public static JToken LatestAsJson()
lock (Gate) return (_lastNonEmpty ?? _current).ToJson();
}

/// <summary>The live selection snapshot, for the editor context-menu commands (Explain / Add to Chat).</summary>
public static SelectionInfo Current
{
get { lock (Gate) return _current; }
}

/// <summary>
/// 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).
/// </summary>
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.EndLineInclusive;
}

try
{
await server.BroadcastNotificationAsync("at_mentioned", @params, CancellationToken.None);
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)
{
Log.Warn($"Add to Chat failed: {e.Message}");
}
}

/// <summary>Record a fresh selection from a focused view and (debounced) push selection_changed.</summary>
public static void Update(IWpfTextView view)
{
Expand Down Expand Up @@ -98,6 +145,14 @@ internal sealed class SelectionInfo

public bool IsEmpty => Text.Length == 0;

/// <summary>
/// The last line the selection actually covers. <see cref="EndLine"/> 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
/// <c>selection_changed</c> / getCurrentSelection JSON keeps the exclusive LSP coordinates.
/// </summary>
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;
Expand Down
44 changes: 44 additions & 0 deletions src/ClaudeCodeVS/VSCommandTable.vsct
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,31 @@
<Group guid="guidClaudeCmdSet" id="ClaudeMenuGroup" priority="0x0600">
<Parent guid="guidSHLMainMenu" id="IDM_VS_MENU_TOOLS" />
</Group>
<Group guid="guidClaudeCmdSet" id="ClaudeCtxMenuGroup" priority="0x0100">
<Parent guid="guidClaudeCmdSet" id="ClaudeCtxMenu" />
</Group>
<!-- The Menu below can't be parented directly to a menu ID - it needs a Group in between,
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. -->
<Group guid="guidClaudeCmdSet" id="ClaudeCtxTopGroup" priority="0x0200">
<Parent guid="guidSHLMainMenu" id="IDM_VS_CTXT_CODEWIN" />
</Group>
</Groups>

<!-- Editor right-click submenu: "Claude Code" -> Explain / Add to Chat. Named for the brand,
matching the Tools and View > Other Windows entries (one brand, not a third variant). -->
<Menus>
<Menu guid="guidClaudeCmdSet" id="ClaudeCtxMenu" priority="0x0001" type="Menu">
<Parent guid="guidClaudeCmdSet" id="ClaudeCtxTopGroup" />
<Strings>
<ButtonText>Claude Code</ButtonText>
<CommandName>Claude Code</CommandName>
</Strings>
</Menu>
</Menus>

<Buttons>
<Button guid="guidClaudeCmdSet" id="LaunchClaudeId" priority="0x0100" type="Button">
<Parent guid="guidClaudeCmdSet" id="ClaudeMenuGroup" />
Expand All @@ -36,15 +59,36 @@
<ButtonText>Claude Code</ButtonText>
</Strings>
</Button>
<Button guid="guidClaudeCmdSet" id="ExplainId" priority="0x0100" type="Button">
<Parent guid="guidClaudeCmdSet" id="ClaudeCtxMenuGroup" />
<Icon guid="ImageCatalogGuid" id="StatusHelp" />
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>Explain</ButtonText>
</Strings>
</Button>
<Button guid="guidClaudeCmdSet" id="AddToChatId" priority="0x0110" type="Button">
<Parent guid="guidClaudeCmdSet" id="ClaudeCtxMenuGroup" />
<Icon guid="ImageCatalogGuid" id="Comment" />
<CommandFlag>IconIsMoniker</CommandFlag>
<Strings>
<ButtonText>Add to Chat</ButtonText>
</Strings>
</Button>
</Buttons>
</Commands>

<Symbols>
<GuidSymbol name="guidClaudeCodeVsPackage" value="{d9032717-8a83-4ab5-9b63-2fe9d9a78481}" />
<GuidSymbol name="guidClaudeCmdSet" value="{9495bbbb-756d-4dc4-807d-1408d50e7d33}">
<IDSymbol name="ClaudeMenuGroup" value="0x1020" />
<IDSymbol name="ClaudeCtxMenu" value="0x1021" />
<IDSymbol name="ClaudeCtxMenuGroup" value="0x1022" />
<IDSymbol name="ClaudeCtxTopGroup" value="0x1023" />
<IDSymbol name="LaunchClaudeId" value="0x0100" />
<IDSymbol name="ShowPanelId" value="0x0101" />
<IDSymbol name="ExplainId" value="0x0102" />
<IDSymbol name="AddToChatId" value="0x0103" />
</GuidSymbol>
</Symbols>
</CommandTable>