Skip to content

Commit d2982ee

Browse files
Merge pull request #418 from erikdarlingdata/dev
Release v1.19.1
2 parents d443228 + 8224d8e commit d2982ee

27 files changed

Lines changed: 351 additions & 101 deletions

.github/workflows/claude-code-review.yml

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,15 @@ jobs:
3232
# Drafts aren't ready for review, and a fork PR gets no secrets (so
3333
# CLAUDE_CODE_OAUTH_TOKEN would be empty) and a read-only token it could
3434
# not post with. Skip rather than fail a contributor's PR with a red X.
35+
#
36+
# Also skip the dev -> main release PR. It is an aggregation of commits that
37+
# were each already reviewed on their own PR, so re-reviewing the whole
38+
# release adds nothing — and the diff is large enough that it reliably
39+
# exhausts the turn budget and fails, putting a red X on the release.
3540
if: |
3641
github.event.pull_request.draft == false &&
37-
github.event.pull_request.head.repo.full_name == github.repository
42+
github.event.pull_request.head.repo.full_name == github.repository &&
43+
!(github.event.pull_request.head.ref == 'dev' && github.event.pull_request.base.ref == 'main')
3844
runs-on: ubuntu-latest
3945

4046
steps:
@@ -91,14 +97,16 @@ jobs:
9197
`confirmed: true`) to flag specific lines.
9298
Only post GitHub comments - do not return review text as a message.
9399
94-
# --allowedTools REPLACES the default tool set rather than adding to it.
95-
# Without Read/Grep/Glob the reviewer cannot open a single file in the
96-
# checked-out branch, which is exactly what the prompt above asks it to
97-
# do. PR #399 burned all 20 turns on denied calls
98-
# (permission_denials_count was 10) and failed without posting anything.
100+
# --allowedTools REPLACES the default tool set rather than adding to it,
101+
# so anything omitted here is denied at runtime. A denied call still
102+
# consumes a turn, so a too-narrow list burns the budget and the review
103+
# fails having posted nothing — that is how #399 (10 denials) and the
104+
# v1.19.0 release PR (20 denials) both died.
105+
#
106+
# Everything here is read-only except the two comment-posting tools.
99107
claude_args: |
100-
--max-turns 40
101-
--allowedTools "Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(git diff:*),Bash(git log:*)"
108+
--max-turns 60
109+
--allowedTools "Read,Grep,Glob,TodoWrite,mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh api:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git status:*),Bash(ls:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(wc:*),Bash(find:*),Bash(rg:*)"
102110
103111
# Fork PRs: GitHub withholds repository secrets from `pull_request` runs on
104112
# forks and issues a read-only GITHUB_TOKEN, so this workflow cannot review

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ authors:
99
website: "https://erikdarling.com"
1010
repository-code: "https://github.com/erikdarlingdata/PerformanceStudio"
1111
license: MIT
12-
version: "1.19.0"
13-
date-released: "2026-07-25"
12+
version: "1.19.1"
13+
date-released: "2026-07-29"
1414
keywords:
1515
- sql-server
1616
- execution-plan

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Tests and server/ projects are outside src/ and are unaffected.
1616
-->
1717
<PropertyGroup>
18-
<Version>1.19.0</Version>
18+
<Version>1.19.1</Version>
1919
<Authors>Erik Darling</Authors>
2020
<Company>Darling Data LLC</Company>
2121
<Product>Performance Studio</Product>

src/PlanViewer.App/AboutWindow.axaml.cs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.Runtime.InteropServices;
1111
using Avalonia.Controls;
1212
using Avalonia.Input;
13-
using Avalonia.Input.Platform;
1413
using Avalonia.Interactivity;
1514
using PlanViewer.App.Mcp;
1615
using PlanViewer.App.Services;
@@ -111,12 +110,9 @@ private async void CopyMcpCommand_Click(object? sender, RoutedEventArgs e)
111110
{
112111
var port = int.TryParse(McpPortInput.Text, out var p) && p >= 1024 && p <= 65535 ? p : 5152;
113112
var command = $"claude mcp add --transport streamable-http --scope user performance-studio http://localhost:{port}/";
114-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
115-
if (clipboard != null)
116-
{
117-
await clipboard.SetTextAsync(command);
118-
McpCopyStatus.Text = "Copied to clipboard!";
119-
}
113+
McpCopyStatus.Text = await ClipboardHelper.TrySetTextAsync(this, command)
114+
? "Copied to clipboard!"
115+
: "Clipboard busy - try again";
120116
}
121117

122118
private string? _updateUrl;

src/PlanViewer.App/App.axaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public override void Initialize()
2121

2222
public override void OnFrameworkInitializationCompleted()
2323
{
24+
// Before any window exists: route every TextBox clipboard operation through
25+
// the guarded helper so a locked clipboard can't crash the app (issue #415).
26+
TextBoxClipboardGuard.Register();
27+
2428
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
2529
{
2630
desktop.MainWindow = new MainWindow();

src/PlanViewer.App/Controls/PlanViewerControl.Interaction.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Avalonia.Interactivity;
88
using Avalonia.Media;
99
using Avalonia.Platform.Storage;
10+
using PlanViewer.App.Services;
1011
using PlanViewer.Core.Models;
1112

1213
namespace PlanViewer.App.Controls;
@@ -138,12 +139,8 @@ private ContextMenu BuildCanvasContextMenu()
138139
return menu;
139140
}
140141

141-
private async System.Threading.Tasks.Task SetClipboardTextAsync(string text)
142-
{
143-
var topLevel = TopLevel.GetTopLevel(this);
144-
if (topLevel?.Clipboard != null)
145-
await topLevel.Clipboard.SetTextAsync(text);
146-
}
142+
private System.Threading.Tasks.Task SetClipboardTextAsync(string text)
143+
=> ClipboardHelper.TrySetTextAsync(this, text);
147144

148145
private void ZoomIn_Click(object? sender, RoutedEventArgs e) => SetZoom(_zoomLevel + ZoomStep);
149146

src/PlanViewer.App/Controls/PlanViewerControl.Schema.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Avalonia.Media;
1111
using AvaloniaEdit.TextMate;
1212
using Microsoft.Data.SqlClient;
13+
using PlanViewer.App.Services;
1314
using PlanViewer.Core.Interfaces;
1415
using PlanViewer.Core.Models;
1516
using PlanViewer.Core.Services;
@@ -117,19 +118,13 @@ private void ShowSchemaResult(string title, string content)
117118
var copyItem = new MenuItem { Header = "Copy" };
118119
copyItem.Click += async (_, _) =>
119120
{
120-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
121-
if (clipboard == null) return;
122121
var sel = editor.TextArea.Selection;
123122
if (!sel.IsEmpty)
124-
await clipboard.SetTextAsync(sel.GetText());
123+
await ClipboardHelper.TrySetTextAsync(this, sel.GetText());
125124
};
126125
var copyAllItem = new MenuItem { Header = "Copy All" };
127126
copyAllItem.Click += async (_, _) =>
128-
{
129-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
130-
if (clipboard == null) return;
131-
await clipboard.SetTextAsync(editor.Text);
132-
};
127+
await ClipboardHelper.TrySetTextAsync(this, editor.Text);
133128
var selectAllItem = new MenuItem { Header = "Select All" };
134129
selectAllItem.Click += (_, _) => editor.SelectAll();
135130
editor.TextArea.ContextMenu = new ContextMenu

src/PlanViewer.App/Controls/PlanViewerControl.Statements.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Avalonia.Interactivity;
99
using Avalonia.Layout;
1010
using Avalonia.Media;
11+
using PlanViewer.App.Services;
1112
using PlanViewer.Core.Models;
1213
using PlanViewer.Core.Services;
1314

@@ -168,9 +169,7 @@ private async void CopyStatementText_Click(object? sender, RoutedEventArgs e)
168169
var text = row.Statement.StatementText;
169170
if (string.IsNullOrEmpty(text)) return;
170171

171-
var topLevel = TopLevel.GetTopLevel(this);
172-
if (topLevel?.Clipboard != null)
173-
await topLevel.Clipboard.SetTextAsync(text);
172+
await ClipboardHelper.TrySetTextAsync(this, text);
174173
}
175174

176175
private void OpenInEditor_Click(object? sender, RoutedEventArgs e)

src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ public PlanViewerControl()
164164
_zoomTransform = (ScaleTransform)layoutTransform.LayoutTransform!;
165165

166166
Helpers.DataGridBehaviors.Attach(StatementsGrid);
167+
Helpers.DataGridBehaviors.AttachCopyGuard(StatementsGrid,
168+
item => item is StatementRow row ? row.Statement.StatementText : null);
167169

168170
// Wire minimap resize grip (defined in AXAML, not in canvas)
169171
MinimapResizeGrip.PointerPressed += MinimapResizeGrip_PointerPressed;

src/PlanViewer.App/Controls/QuerySessionControl.Editor.cs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.Xml.Linq;
1111
using Avalonia.Controls;
1212
using Avalonia.Input;
13-
using Avalonia.Input.Platform;
1413
using Avalonia.Interactivity;
1514
using Avalonia.Layout;
1615
using Avalonia.Media;
@@ -42,31 +41,27 @@ private void SetupEditorContextMenu()
4241
var cutItem = new MenuItem { Header = "Cut" };
4342
cutItem.Click += async (_, _) =>
4443
{
45-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
46-
if (clipboard == null) return;
4744
var selection = QueryEditor.TextArea.Selection;
4845
if (selection.IsEmpty) return;
4946
var text = selection.GetText();
50-
await clipboard.SetTextAsync(text);
51-
selection.ReplaceSelectionWithText("");
47+
// Only remove the selection once the text has actually reached the
48+
// clipboard; a failed copy must not destroy the user's text.
49+
if (await ClipboardHelper.TrySetTextAsync(this, text))
50+
selection.ReplaceSelectionWithText("");
5251
};
5352

5453
var copyItem = new MenuItem { Header = "Copy" };
5554
copyItem.Click += async (_, _) =>
5655
{
57-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
58-
if (clipboard == null) return;
5956
var selection = QueryEditor.TextArea.Selection;
6057
if (selection.IsEmpty) return;
61-
await clipboard.SetTextAsync(selection.GetText());
58+
await ClipboardHelper.TrySetTextAsync(this, selection.GetText());
6259
};
6360

6461
var pasteItem = new MenuItem { Header = "Paste" };
6562
pasteItem.Click += async (_, _) =>
6663
{
67-
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
68-
if (clipboard == null) return;
69-
var text = await clipboard.TryGetTextAsync();
64+
var text = await ClipboardHelper.TryGetTextAsync(this);
7065
if (string.IsNullOrEmpty(text)) return;
7166
QueryEditor.TextArea.PerformTextInput(text);
7267
};

0 commit comments

Comments
 (0)