Skip to content
Open
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
59 changes: 29 additions & 30 deletions src/ClaudeCodeVS/Ui/ClaudeToolWindowControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public ClaudeToolWindowControl()
SetResourceReference(BackgroundProperty, VsBrushes.ToolWindowBackgroundKey);
SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
SetResourceReference(TextBlock.ForegroundProperty, VsBrushes.ToolWindowTextKey);
FontScale.BindRoot(this);

var root = new Grid { Margin = new Thickness(10, 8, 10, 8) };
for (int i = 0; i < 7; i++)
Expand All @@ -77,16 +78,16 @@ public ClaudeToolWindowControl()

// ---- Row 0: header (title + status pill) ----
var header = new StackPanel { Margin = new Thickness(0, 0, 0, 8) };
header.Children.Add(new TextBlock { Text = "Claude Code", FontSize = 15, FontWeight = FontWeights.SemiBold });
header.Children.Add(Font(new TextBlock { Text = "Claude Code", FontWeight = FontWeights.SemiBold }, 1.25));

var statusRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
_dot = new Ellipse { Width = 9, Height = 9, Fill = DotIdle, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0) };
_statusLine = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontSize = 12 };
_statusLine = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center }, 1.0);
statusRow.Children.Add(_dot);
statusRow.Children.Add(_statusLine);
header.Children.Add(statusRow);

_endpointLine = new TextBlock { FontSize = 11, Opacity = 0.65, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 1, 0, 0) };
_endpointLine = Font(new TextBlock { Opacity = 0.65, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 1, 0, 0) }, 0.92);
header.Children.Add(_endpointLine);
Grid.SetRow(header, 0);

Expand Down Expand Up @@ -178,16 +179,15 @@ public ClaudeToolWindowControl()
// Surfaces the otherwise-silent gap where the IDE WebSocket connected but vs-debug/vs-semantic/
// tests never loaded (Claude launched outside the workspace, or project servers unapproved).
var warnStack = new StackPanel();
_toolsWarningTitle = new TextBlock
_toolsWarningTitle = Font(new TextBlock
{
Text = "⚠ Workspace hooks & tools didn't load for this session",
FontSize = 12,
FontWeight = FontWeights.SemiBold,
Foreground = WarnText,
TextWrapping = TextWrapping.Wrap,
};
}, 1.0);
warnStack.Children.Add(_toolsWarningTitle);
_toolsWarningText = new TextBlock { FontSize = 11.5, Opacity = 0.85, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0) };
_toolsWarningText = Font(new TextBlock { Opacity = 0.85, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0) }, 0.96);
warnStack.Children.Add(_toolsWarningText);
var warnButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 6, 0, 0) };
warnButtons.Children.Add(MakeButton("Relaunch Claude Code", () => { _ = BridgeStatus.LaunchAction?.Invoke(); }));
Expand All @@ -206,14 +206,14 @@ public ClaudeToolWindowControl()
Grid.SetRow(_toolsWarningCard, 2);

// ---- Row 3: stats card ----
_editsLine = new TextBlock { FontSize = 12, TextWrapping = TextWrapping.Wrap };
_debugLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) };
_latestLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 0) };
_sessionLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) };
_editsLine = Font(new TextBlock { TextWrapping = TextWrapping.Wrap }, 1.0);
_debugLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }, 1.0);
_latestLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 0) }, 1.0);
_sessionLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }, 1.0);

// Cost is an estimate, so it's gated behind a toggle rather than shown by default.
_costButton = MakeButton("≈ Show est. cost", ToggleCost);
_costText = new TextBlock { FontSize = 12, VerticalAlignment = VerticalAlignment.Center, Opacity = 0.9, Margin = new Thickness(8, 0, 0, 0), Visibility = Visibility.Collapsed };
_costText = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.9, Margin = new Thickness(8, 0, 0, 0), Visibility = Visibility.Collapsed }, 1.0);
_costRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 6, 0, 0), Visibility = Visibility.Collapsed };
_costRow.Children.Add(_costButton);
_costRow.Children.Add(_costText);
Expand All @@ -235,7 +235,7 @@ public ClaudeToolWindowControl()
Grid.SetRow(statsCard, 3);

// ---- Row 4: pending diffs (collapsed when none) ----
_pendingText = new TextBlock { FontSize = 12, TextWrapping = TextWrapping.Wrap };
_pendingText = Font(new TextBlock { TextWrapping = TextWrapping.Wrap }, 1.0);
_pendingCard = new Border
{
Background = Chip,
Expand All @@ -251,13 +251,12 @@ public ClaudeToolWindowControl()
// Screenshots can't be pasted into the CLI on Windows (open upstream gap), so the panel is the
// paste/drop point: stage the file, then push an at_mentioned so the reference lands in the
// CLI's composer with no path typing. Chips show what's staged; × removes, click re-mentions.
var attachHint = new TextBlock
var attachHint = Font(new TextBlock
{
Text = "📎 Attach — drop images/files here, or",
FontSize = 11.5,
Opacity = 0.75,
VerticalAlignment = VerticalAlignment.Center,
};
}, 0.96);
var attachHeader = new StackPanel { Orientation = Orientation.Horizontal };
attachHeader.Children.Add(attachHint);
var pasteBtn = MakeButton("Paste", PasteFromClipboard);
Expand All @@ -272,14 +271,13 @@ public ClaudeToolWindowControl()
_attachClear.Visibility = Visibility.Collapsed;
attachHeader.Children.Add(_attachClear);
// A live "what will this cost" readout for the staged set - same estimate language as the cost row.
_attachSummary = new TextBlock
_attachSummary = Font(new TextBlock
{
FontSize = 11.5,
Opacity = 0.65,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(2, 0, 0, 0),
Visibility = Visibility.Collapsed,
};
}, 0.96);
_attachSummary.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
attachHeader.Children.Add(_attachSummary);

Expand All @@ -305,7 +303,7 @@ public ClaudeToolWindowControl()
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, (s, e) => PasteFromClipboard()));

// ---- Row 6: feed label ----
var feedLabel = new TextBlock { Text = "ACTIVITY", FontSize = 10, FontWeight = FontWeights.SemiBold, Opacity = 0.55, Margin = new Thickness(2, 0, 0, 4) };
var feedLabel = Font(new TextBlock { Text = "ACTIVITY", FontWeight = FontWeights.SemiBold, Opacity = 0.55, Margin = new Thickness(2, 0, 0, 4) }, 0.83);
Grid.SetRow(feedLabel, 6);

// ---- Row 7: curated activity feed ----
Expand Down Expand Up @@ -386,7 +384,7 @@ private void AddFeedLine(LogLevel level, string text)
// Raw JSON frames and notification noise stay in the Output pane; keep the panel readable.
if (level == LogLevel.Frame || level == LogLevel.Event) return;

var tb = new TextBlock { Text = text, FontFamily = Mono, FontSize = 11.5, TextWrapping = TextWrapping.NoWrap };
var tb = Font(new TextBlock { Text = text, FontFamily = Mono, TextWrapping = TextWrapping.NoWrap }, 0.96);
if (level == LogLevel.Error) tb.Foreground = ErrText;
else if (level == LogLevel.Warn) tb.Foreground = WarnText;
else tb.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
Expand Down Expand Up @@ -664,29 +662,27 @@ private void RefreshAttachments()
var it = item; // capture per chip
var est = it.EstTokens is long e ? $"\n≈ {Tok(e)} tokens when read (estimate)" : "";
var toolNote = it.NeedsTool ? "\nNot a format Claude reads directly - it will use a script/tool on it." : "";
var name = new TextBlock
var name = Font(new TextBlock
{
Text = (it.IsImage ? "🖼 " : it.NeedsTool ? "🧰 " : "📄 ") + it.FileName + (it.Sent ? "" : " ⏳"),
FontSize = 11.5,
VerticalAlignment = VerticalAlignment.Center,
Cursor = Cursors.Hand,
ToolTip = it.MentionPath + est + toolNote + (it.Sent
? "\nClick to @-mention it again."
: "\nStaged - sends when Claude connects. Click to retry."),
};
}, 0.96);
name.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
name.MouseLeftButtonUp += (s, e) => _ = Task.Run(() => Attachments.AttachmentService.ResendAsync(it));

var close = new TextBlock
var close = Font(new TextBlock
{
Text = "✕",
FontSize = 11.5,
Margin = new Thickness(7, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
Cursor = Cursors.Hand,
Opacity = 0.65,
ToolTip = it.WasCopied ? "Remove (deletes the staged copy)" : "Remove from the tray",
};
}, 0.96);
close.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
close.MouseLeftButtonUp += (s, e) => Attachments.AttachmentService.Remove(it);

Expand Down Expand Up @@ -731,6 +727,10 @@ private static string ShortModel(string model)
return model;
}

/// <summary>Scales an element's FontSize relative to VS's Environment Font size (see <see cref="FontScale"/>).</summary>
private static T Font<T>(T el, double ratio) where T : FrameworkElement
=> FontScale.Bind(el, typeof(ClaudeToolWindowControl), ratio);

/// <summary>A flat, non-selectable list item (so the feed reads like a log, not a selectable list).</summary>
private static Style FlatItemStyle()
{
Expand All @@ -747,16 +747,15 @@ private static Style FlatItemStyle()
/// <summary>A flat, theme-aware button: a rounded chip that lightens on hover, themed text.</summary>
private static Button MakeButton(string text, Action onClick)
{
var b = new Button
var b = Font(new Button
{
Content = text,
Margin = new Thickness(0, 0, 6, 0),
Padding = new Thickness(10, 3, 10, 3),
FontSize = 12,
Cursor = Cursors.Hand,
Background = Chip,
BorderThickness = new Thickness(0),
};
}, 1.0);
b.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);

var border = new System.Windows.FrameworkElementFactory(typeof(Border));
Expand Down
7 changes: 6 additions & 1 deletion src/ClaudeCodeVS/Ui/ComposeDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ private ComposeDialog(string initialText)

SetResourceReference(BackgroundProperty, VsBrushes.ToolWindowBackgroundKey);
SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey);
FontScale.BindRoot(this);

var grid = new Grid { Margin = new Thickness(12) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
Expand Down Expand Up @@ -69,7 +70,7 @@ private ComposeDialog(string initialText)
Grid.SetRow(_box, 1);

var footer = new DockPanel { Margin = new Thickness(0, 8, 0, 0) };
_estimate = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.65, FontSize = 11.5 };
_estimate = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.65 }, 0.96);
_estimate.SetResourceReference(TextBlock.ForegroundProperty, VsBrushes.ToolWindowTextKey);
footer.Children.Add(_estimate);

Expand Down Expand Up @@ -115,6 +116,10 @@ private void UpdateEstimate()
return string.IsNullOrWhiteSpace(text) ? null : text;
}

/// <summary>Scales an element's FontSize relative to VS's Environment Font size (see <see cref="FontScale"/>).</summary>
private static T Font<T>(T el, double ratio) where T : FrameworkElement
=> FontScale.Bind(el, typeof(ComposeDialog), ratio);

/// <summary>Minimal ICommand for the Ctrl+Enter accelerator (no framework helper in scope).</summary>
private sealed class RelayCommand : ICommand
{
Expand Down
43 changes: 43 additions & 0 deletions src/ClaudeCodeVS/Ui/FontScale.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Documents;
using Microsoft.VisualStudio.Shell;

namespace ClaudeCodeVs.Ui;

/// <summary>
/// Ties this extension's hand-built WPF panels to VS's own Environment Font size (Tools > Options >
/// Environment > Fonts and Colors), instead of the hardcoded point values every element used before.
/// <see cref="BindRoot"/> makes a panel's own FontSize track <c>VsFonts.EnvironmentFontSizeKey</c>;
/// <see cref="Bind{T}"/> scales a descendant relative to that root so the panel's size hierarchy
/// (title vs. body vs. caption) survives, and both update live if the user changes the VS setting.
/// </summary>
internal static class FontScale
{
private static readonly IValueConverter Ratio = new RatioConverter();

public static void BindRoot(FrameworkElement root)
=> root.SetResourceReference(TextElement.FontSizeProperty, VsFonts.EnvironmentFontSizeKey);

public static T Bind<T>(T el, Type rootType, double ratio = 1.0) where T : FrameworkElement
{
el.SetBinding(TextElement.FontSizeProperty, new Binding
{
Path = new PropertyPath(TextElement.FontSizeProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, rootType, 1),
Converter = Ratio,
ConverterParameter = ratio,
});
return el;
}

private sealed class RatioConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is double d && parameter is double r ? d * r : value;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
}