From 28e75e7d976eeb5104a4cf555666093d89ac30fd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 00:59:32 +0000
Subject: [PATCH 1/2] Initial plan
From 1f08594779e5ccefe5812ab0ce24f08c4d2e9550 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 01:14:14 +0000
Subject: [PATCH 2/2] Replace eval-based JS interop with dedicated
polypilot-interop.js module
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
---
.../Components/ExpandedSessionView.razor | 94 +-----
PolyPilot/Components/Layout/MainLayout.razor | 27 +-
.../Components/Layout/SessionSidebar.razor | 21 +-
PolyPilot/Components/Pages/Dashboard.razor | 156 +---------
PolyPilot/Components/Pages/Settings.razor | 24 +-
PolyPilot/wwwroot/index.html | 1 +
PolyPilot/wwwroot/js/polypilot-interop.js | 275 ++++++++++++++++++
7 files changed, 308 insertions(+), 290 deletions(-)
create mode 100644 PolyPilot/wwwroot/js/polypilot-interop.js
diff --git a/PolyPilot/Components/ExpandedSessionView.razor b/PolyPilot/Components/ExpandedSessionView.razor
index e91f6e3b..2298e3b0 100644
--- a/PolyPilot/Components/ExpandedSessionView.razor
+++ b/PolyPilot/Components/ExpandedSessionView.razor
@@ -497,28 +497,9 @@
$"{EscapeHtml(s.Source)}" +
desc + "";
}));
- var jsHtml = EscapeForJs(rows);
- var headerHtml = EscapeForJs("
Available Skills
");
- await JS.InvokeVoidAsync("eval", $@"
- (function(){{
- var old = document.getElementById('skills-popup-overlay');
- if(old) old.remove();
- var trigger = document.querySelector('[data-trigger=""skills""]');
- var rect = trigger ? trigger.getBoundingClientRect() : {{left:20,bottom:60}};
- var ov = document.createElement('div');
- ov.id = 'skills-popup-overlay';
- ov.style.cssText = 'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)';
- ov.onclick = function(){{ ov.remove(); }};
- var popup = document.createElement('div');
- var left = Math.max(8, Math.min(rect.left, window.innerWidth - 368));
- var bottom = window.innerHeight - rect.top + 8;
- popup.style.cssText = 'position:fixed;bottom:'+bottom+'px;left:'+left+'px;z-index:9999;background:#1e1e2e;border:1px solid #45475a;border-radius:10px;padding:6px 0;min-width:240px;max-width:360px;max-height:50vh;overflow-y:auto;box-shadow:0 -4px 20px rgba(0,0,0,0.5)';
- popup.innerHTML = '{headerHtml}{jsHtml}';
- popup.onclick = function(e){{ e.stopPropagation(); }};
- ov.appendChild(popup);
- document.body.appendChild(ov);
- }})()
- ");
+ await JS.InvokeVoidAsync("showPopup", "[data-trigger=\"skills\"]",
+ "Available Skills
",
+ rows);
}
private async Task ShowAgentsPopup()
@@ -534,28 +515,9 @@
$"{EscapeHtml(a.Source)}" +
desc + "";
}));
- var jsHtml = EscapeForJs(rows);
- var headerHtml = EscapeForJs("Available Agents
");
- await JS.InvokeVoidAsync("eval", $@"
- (function(){{
- var old = document.getElementById('skills-popup-overlay');
- if(old) old.remove();
- var trigger = document.querySelector('[data-trigger=""agents""]');
- var rect = trigger ? trigger.getBoundingClientRect() : {{left:20,bottom:60}};
- var ov = document.createElement('div');
- ov.id = 'skills-popup-overlay';
- ov.style.cssText = 'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)';
- ov.onclick = function(){{ ov.remove(); }};
- var popup = document.createElement('div');
- var left = Math.max(8, Math.min(rect.left, window.innerWidth - 368));
- var bottom = window.innerHeight - rect.top + 8;
- popup.style.cssText = 'position:fixed;bottom:'+bottom+'px;left:'+left+'px;z-index:9999;background:#1e1e2e;border:1px solid #45475a;border-radius:10px;padding:6px 0;min-width:240px;max-width:360px;max-height:50vh;overflow-y:auto;box-shadow:0 -4px 20px rgba(0,0,0,0.5)';
- popup.innerHTML = '{headerHtml}{jsHtml}';
- popup.onclick = function(e){{ e.stopPropagation(); }};
- ov.appendChild(popup);
- document.body.appendChild(ov);
- }})()
- ");
+ await JS.InvokeVoidAsync("showPopup", "[data-trigger=\"agents\"]",
+ "Available Agents
",
+ rows);
}
private async Task ShowPromptsPopup()
@@ -571,39 +533,10 @@
$"{EscapeHtml(p.SourceLabel)}" +
desc + "";
}));
- var jsHtml = EscapeForJs(rows);
- var headerHtml = EscapeForJs("Available Prompts (click to use)
");
- await JS.InvokeVoidAsync("eval", $@"
- (function(){{
- var old = document.getElementById('skills-popup-overlay');
- if(old) old.remove();
- var trigger = document.querySelector('[data-trigger=""prompts""]');
- var rect = trigger ? trigger.getBoundingClientRect() : {{left:20,bottom:60}};
- var ov = document.createElement('div');
- ov.id = 'skills-popup-overlay';
- ov.style.cssText = 'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)';
- ov.onclick = function(){{ ov.remove(); }};
- var popup = document.createElement('div');
- var left = Math.max(8, Math.min(rect.left, window.innerWidth - 368));
- var bottom = window.innerHeight - rect.top + 8;
- popup.style.cssText = 'position:fixed;bottom:'+bottom+'px;left:'+left+'px;z-index:9999;background:#1e1e2e;border:1px solid #45475a;border-radius:10px;padding:6px 0;min-width:240px;max-width:360px;max-height:50vh;overflow-y:auto;box-shadow:0 -4px 20px rgba(0,0,0,0.5)';
- popup.innerHTML = '{headerHtml}{jsHtml}';
- popup.onclick = function(e){{
- var row = e.target.closest('.prompt-row');
- if(row){{
- var name = row.getAttribute('data-prompt');
- ov.remove();
- var inputEl = document.querySelector('[data-session=""{EscapeForJs(Session.Name)}""] textarea');
- if(inputEl){{
- inputEl.value = '/prompt use ' + name;
- inputEl.dispatchEvent(new Event('input'));
- }}
- }}
- }};
- ov.appendChild(popup);
- document.body.appendChild(ov);
- }})()
- ");
+ await JS.InvokeVoidAsync("showPromptsPopup", "[data-trigger=\"prompts\"]",
+ "Available Prompts (click to use)
",
+ rows,
+ Session.Name);
}
private static string EscapeHtml(string s) =>
@@ -611,13 +544,10 @@
private async Task InsertReflectCommand()
{
- var inputId = EscapeForJs("input-" + Session.Name.Replace(" ", "-"));
- await JS.InvokeVoidAsync("eval", $"var el = document.getElementById('{inputId}'); if(el){{ el.value = '/reflect '; el.focus(); }}");
+ var inputId = "input-" + Session.Name.Replace(" ", "-");
+ await JS.InvokeVoidAsync("focusAndSetValue", inputId, "/reflect ");
}
- private static string EscapeForJs(string s) =>
- s.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\n", " ").Replace("\r", "");
-
private static string TruncateDesc(string desc)
{
desc = desc.Trim();
diff --git a/PolyPilot/Components/Layout/MainLayout.razor b/PolyPilot/Components/Layout/MainLayout.razor
index 6a7d47e8..5c6f7a48 100644
--- a/PolyPilot/Components/Layout/MainLayout.razor
+++ b/PolyPilot/Components/Layout/MainLayout.razor
@@ -47,7 +47,7 @@
UiTheme.SolarizedLight => "solarized-light",
_ => ""
};
- await JS.InvokeVoidAsync("eval", $"document.documentElement.setAttribute('data-theme', '{dataTheme}')");
+ await JS.InvokeVoidAsync("setThemeAttribute", dataTheme);
// Load saved font size and navigate to last page
var uiState = CopilotService.LoadUiState();
@@ -83,33 +83,12 @@
private async Task ApplyFontSize()
{
- await JS.InvokeVoidAsync("eval", $"document.documentElement.style.setProperty('--app-font-size', '{fontSize}px')");
+ await JS.InvokeVoidAsync("setAppFontSize", fontSize);
}
private async Task StartResize(MouseEventArgs e)
{
- await JS.InvokeVoidAsync("eval", $@"
- (function() {{
- var sidebar = document.querySelector('.sidebar.desktop-only');
- if (!sidebar) return;
- var startX = {e.ClientX};
- var startW = sidebar.offsetWidth;
- function onMove(e) {{
- var w = Math.min(Math.max(startW + e.clientX - startX, 200), 600);
- sidebar.style.width = w + 'px';
- }}
- function onUp() {{
- document.removeEventListener('mousemove', onMove);
- document.removeEventListener('mouseup', onUp);
- document.body.style.userSelect = '';
- document.body.style.cursor = '';
- }}
- document.body.style.userSelect = 'none';
- document.body.style.cursor = 'col-resize';
- document.addEventListener('mousemove', onMove);
- document.addEventListener('mouseup', onUp);
- }})();
- ");
+ await JS.InvokeVoidAsync("startSidebarResize", e.ClientX);
}
private void ToggleFlyout()
diff --git a/PolyPilot/Components/Layout/SessionSidebar.razor b/PolyPilot/Components/Layout/SessionSidebar.razor
index 57073921..699bd0b4 100644
--- a/PolyPilot/Components/Layout/SessionSidebar.razor
+++ b/PolyPilot/Components/Layout/SessionSidebar.razor
@@ -794,14 +794,7 @@ else
if (firstRender)
{
// Set up Enter key handler via JS to avoid Blazor round-trips on every keystroke
- await JS.InvokeVoidAsync("eval", @"
- document.getElementById('sessionNameInput')?.addEventListener('keydown', function(e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- document.querySelector('.new-session button')?.click();
- }
- });
- ");
+ await JS.InvokeVoidAsync("wireSessionNameInputEnter");
}
}
@@ -1008,10 +1001,7 @@ else
StateHasChanged();
// Focus the input after render
await Task.Yield();
- await JS.InvokeVoidAsync("eval", @"
- var el = document.getElementById('renameInput');
- if (el) { el.focus(); el.select(); }
- ");
+ await JS.InvokeVoidAsync("focusAndSelect", "renameInput");
}
private async Task CommitRename()
@@ -1044,10 +1034,7 @@ else
renamingGroupId = groupId;
StateHasChanged();
await Task.Yield();
- await JS.InvokeVoidAsync("eval", @"
- var el = document.getElementById('groupRenameInput');
- if (el) { el.focus(); el.select(); }
- ");
+ await JS.InvokeVoidAsync("focusAndSelect", "groupRenameInput");
}
private async Task CommitGroupRename(string groupId)
@@ -1072,7 +1059,7 @@ else
CopilotService.SetActiveSession(null);
CopilotService.SaveUiState("/");
currentPage = "/";
- try { await JS.InvokeVoidAsync("eval", "window.__dashRef?.invokeMethodAsync('JsCollapseToGrid')"); } catch { }
+ try { await JS.InvokeVoidAsync("collapseToGrid"); } catch { }
Nav.NavigateTo("/");
}
diff --git a/PolyPilot/Components/Pages/Dashboard.razor b/PolyPilot/Components/Pages/Dashboard.razor
index 204a6b84..9bae0163 100644
--- a/PolyPilot/Components/Pages/Dashboard.razor
+++ b/PolyPilot/Components/Pages/Dashboard.razor
@@ -626,132 +626,16 @@
{
if (firstRender)
{
- await JS.InvokeVoidAsync("eval", @"
- if (!window.__dashboardKeydownRegistered) {
- window.__dashboardKeydownRegistered = true;
- document.addEventListener('keydown', function(e) {
- var sel = '.card-input input, .card-input textarea, .input-row textarea';
- var isInput = e.target.matches && e.target.matches(sel);
- if (e.key === 'Enter' && !e.shiftKey && isInput) {
- e.preventDefault();
- if (window.__sendPending) return;
- window.__sendPending = true;
- setTimeout(function() { window.__sendPending = false; }, 500);
- var container = e.target.closest('.card-input') || e.target.closest('.input-row');
- if (container) {
- var btn = container.querySelector('.send-btn:not(.stop-btn)') || container.querySelectorAll('button')[container.querySelectorAll('button').length - 1];
- if (btn) btn.click();
- }
- }
- // ArrowUp/Down: command history navigation
- if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isInput && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
- var ta = e.target;
- var atStart = ta.selectionStart === 0 && ta.selectionEnd === 0;
- var atEnd = ta.selectionStart === ta.value.length;
- var card = ta.closest('[data-session]');
- var sessionName = card ? card.dataset.session : '';
- var histNavActive = sessionName && window.__histNavActive && window.__histNavActive[sessionName];
- if ((e.key === 'ArrowUp' && atStart) || (e.key === 'ArrowDown' && (atEnd || histNavActive))) {
- if (sessionName && window.__dashRef) {
- e.preventDefault();
- window.__dashRef.invokeMethodAsync('JsNavigateHistory', sessionName, e.key === 'ArrowUp').then(function(isNav) {
- if (!window.__histNavActive) window.__histNavActive = {};
- window.__histNavActive[sessionName] = isNav;
- }).catch(function() {
- if (window.__histNavActive) window.__histNavActive[sessionName] = false;
- });
- }
- }
- }
- if (e.key === 'Tab' && isInput) {
- e.preventDefault();
- e.stopImmediatePropagation();
- var expandedCard = document.querySelector('.expanded-card');
- if (expandedCard && window.__dashRef) {
- window.__dashRef.invokeMethodAsync('JsCycleExpandedSession', e.shiftKey);
- return;
- }
- var inputs = Array.from(document.querySelectorAll(sel));
- if (inputs.length < 2) return;
- var idx = inputs.indexOf(e.target);
- if (idx < 0) idx = 0;
- idx = e.shiftKey ? (idx - 1 + inputs.length) % inputs.length : (idx + 1) % inputs.length;
- inputs[idx].focus();
- var card = inputs[idx].closest('.session-card');
- if (card && card.dataset.session && window.__dashRef) {
- window.__dashRef.invokeMethodAsync('JsSelectSession', card.dataset.session);
- }
- }
- if ((e.metaKey || e.ctrlKey) && e.key === 'e') {
- e.preventDefault();
- var collapseBtn = document.querySelector('.collapse-card-btn');
- if (collapseBtn) { collapseBtn.click(); return; }
- var card = isInput ? e.target.closest('.session-card') : document.querySelector('.session-card');
- if (card && card.dataset.session && window.__dashRef) {
- window.__dashRef.invokeMethodAsync('JsExpandSession', card.dataset.session);
- }
- }
- if (e.key === 'Escape') {
- var collapseBtn = document.querySelector('.collapse-card-btn');
- if (collapseBtn) collapseBtn.click();
- }
- // ⌘1-9 / Ctrl+1-9: switch to session by index
- if ((e.metaKey || e.ctrlKey) && e.key >= '1' && e.key <= '9') {
- e.preventDefault();
- if (window.__dashRef) {
- window.__dashRef.invokeMethodAsync('JsSwitchToSessionByIndex', parseInt(e.key));
- }
- }
- // ⌘+/⌘- / Ctrl+=/Ctrl+-: font size, ⌘0 reset
- if ((e.metaKey || e.ctrlKey) && (e.key === '=' || e.key === '+' || e.key === '-' || e.key === '0')) {
- e.preventDefault();
- if (window.__dashRef) {
- var delta = (e.key === '=' || e.key === '+') ? 1 : e.key === '-' ? -1 : 0;
- window.__dashRef.invokeMethodAsync('JsChangeFontSize', delta);
- }
- }
- // Ctrl+C: interrupt running session (only when no text selected)
- if (e.ctrlKey && e.key === 'c' && !e.metaKey && !e.shiftKey) {
- var selection = window.getSelection();
- if (!selection || selection.toString().length === 0) {
- e.preventDefault();
- if (window.__dashRef) {
- window.__dashRef.invokeMethodAsync('JsInterruptSession');
- }
- }
- }
- });
- // Reset histNavActive when user edits text, so ArrowDown doesn't
- // overwrite their changes with a history entry.
- document.addEventListener('input', function(e) {
- if (e.target.matches && e.target.matches('.card-input input, .card-input textarea, .input-row textarea')) {
- var card = e.target.closest('[data-session]');
- var sn = card ? card.dataset.session : '';
- if (sn && window.__histNavActive) window.__histNavActive[sn] = false;
- }
- });
- }
- ");
+ await JS.InvokeVoidAsync("ensureDashboardKeyHandlers");
try
{
var dotNetRef = DotNetObjectReference.Create(this);
if (_disposed) { dotNetRef.Dispose(); return; }
- await JS.InvokeVoidAsync("eval", "window.__dashRef = null;");
+ await JS.InvokeVoidAsync("clearDashRef");
_dotNetRef = dotNetRef;
- await JS.InvokeVoidAsync("eval", "window.__setDashRef = function(ref) { window.__dashRef = ref; };");
if (_disposed) return;
await JS.InvokeVoidAsync("__setDashRef", _dotNetRef);
- await JS.InvokeVoidAsync("eval", @"
- if (!window.__textareaAutoResize) {
- window.__textareaAutoResize = true;
- document.addEventListener('input', function(e) {
- if (e.target.tagName === 'TEXTAREA' && e.target.closest('.input-row')) {
- e.target.style.height = 'auto';
- e.target.style.height = Math.min(e.target.scrollHeight, 150) + 'px';
- }
- });
- }
- ");
+ await JS.InvokeVoidAsync("ensureTextareaAutoResize");
}
catch (ObjectDisposedException) { }
}
@@ -790,18 +674,7 @@
if (!_loadMoreObserverInitialized || forceScroll)
{
_loadMoreObserverInitialized = true;
- _ = JS.InvokeVoidAsync("eval", @"
- if (!window.__loadMoreObserver) {
- window.__loadMoreObserver = new IntersectionObserver(function(entries) {
- entries.forEach(function(entry) {
- if (entry.isIntersecting) entry.target.click();
- });
- }, { threshold: 0.1 });
- }
- document.querySelectorAll('.messages .load-more-btn').forEach(function(btn) {
- if (!btn.__observed) { btn.__observed = true; window.__loadMoreObserver.observe(btn); }
- });
- ");
+ _ = JS.InvokeVoidAsync("ensureLoadMoreObserver");
}
}
@@ -1246,16 +1119,7 @@
activityBySession.Remove(sessionName);
// Force scroll to bottom immediately after sending
- await JS.InvokeVoidAsync("eval", @"
- document.querySelectorAll('.card-messages, .messages').forEach(function(el) {
- el.scrollTop = el.scrollHeight;
- });
- setTimeout(function() {
- document.querySelectorAll('.card-messages, .messages').forEach(function(el) {
- el.scrollTop = el.scrollHeight;
- });
- }, 100);
- ");
+ await JS.InvokeVoidAsync("scrollMessagesToBottom");
try
{
@@ -2321,10 +2185,7 @@
await Task.Yield();
StateHasChanged();
await Task.Yield();
- await JS.InvokeVoidAsync("eval", @"
- var el = document.getElementById('cardRenameInput');
- if (el) { el.focus(); el.select(); }
- ");
+ await JS.InvokeVoidAsync("focusAndSelect", "cardRenameInput");
}
private async Task CommitCardRename()
@@ -2698,8 +2559,7 @@
var (text, cursorAtStart) = result.Value;
var inputId = $"input-{sessionName.Replace(" ", "-")}";
- var cursorExpr = cursorAtStart ? "0" : "el.value.length";
- await JS.InvokeVoidAsync("eval", $"(function(){{ var el = document.getElementById('{inputId}'); if(el){{ el.value = {System.Text.Json.JsonSerializer.Serialize(text)}; var p = {cursorExpr}; el.setSelectionRange(p, p); }} }})()");
+ await JS.InvokeVoidAsync("setInputValue", inputId, text, cursorAtStart);
return hist.IsNavigating;
}
@@ -2942,7 +2802,7 @@
foreach (var images in pendingImagesBySession.Values)
foreach (var img in images)
try { File.Delete(img.TempPath); } catch { }
- try { await JS.InvokeVoidAsync("eval", "window.__dashRef = null;"); } catch { }
+ try { await JS.InvokeVoidAsync("clearDashRef"); } catch { }
_dotNetRef?.Dispose();
}
}
diff --git a/PolyPilot/Components/Pages/Settings.razor b/PolyPilot/Components/Pages/Settings.razor
index a445e308..8ab5f942 100644
--- a/PolyPilot/Components/Pages/Settings.razor
+++ b/PolyPilot/Components/Pages/Settings.razor
@@ -634,7 +634,7 @@
private async Task ClearSearch()
{
searchQuery = "";
- await JS.InvokeVoidAsync("eval", "document.getElementById('settings-search').value = ''");
+ await JS.InvokeVoidAsync("clearElementValue", "settings-search");
}
private bool SectionVisible(string keywords)
@@ -713,22 +713,8 @@
private async Task InitializeAfterRenderAsync()
{
_selfRef = DotNetObjectReference.Create(this);
- await JS.InvokeVoidAsync("eval", "window.__setSettingsRef = function(ref) { window.__settingsRef = ref; };");
await JS.InvokeVoidAsync("__setSettingsRef", _selfRef);
- await JS.InvokeVoidAsync("eval", @"
- (function() {
- var el = document.getElementById('settings-search');
- if (!el || el.__searchWired) return;
- el.__searchWired = true;
- var timer = null;
- el.addEventListener('input', function() {
- clearTimeout(timer);
- var val = el.value;
- timer = setTimeout(function() {
- if (window.__settingsRef) window.__settingsRef.invokeMethodAsync('JsUpdateSearch', val);
- }, 150);
- });
- })()");
+ await JS.InvokeVoidAsync("wireSettingsSearch");;
// Run slow detection tasks in parallel (after first paint)
var cliInfoTask = Task.Run(CopilotService.GetCliSourceInfo);
@@ -755,7 +741,7 @@
DevTunnelService.OnStateChanged -= OnTunnelStateChanged;
GitAutoUpdate.OnStateChanged -= OnAutoUpdateStateChanged;
FiestaService.OnStateChanged -= OnFiestaStateChanged;
- _ = JS.InvokeVoidAsync("eval", "window.__settingsRef = null;");
+ _ = JS.InvokeVoidAsync("clearSettingsRef");
_selfRef?.Dispose();
}
@@ -987,7 +973,7 @@
private async Task ApplyFontSize()
{
- await JS.InvokeVoidAsync("eval", $"document.documentElement.style.setProperty('--app-font-size', '{fontSize}px')");
+ await JS.InvokeVoidAsync("setAppFontSize", fontSize);
CopilotService.SaveUiState("/settings", fontSize: fontSize);
}
@@ -1118,7 +1104,7 @@
UiTheme.SolarizedLight => "solarized-light",
_ => ""
};
- await JS.InvokeVoidAsync("eval", $"document.documentElement.setAttribute('data-theme', '{dataTheme}')");
+ await JS.InvokeVoidAsync("setThemeAttribute", dataTheme);
}
private string GetThemeLabel(UiTheme theme) => theme switch
diff --git a/PolyPilot/wwwroot/index.html b/PolyPilot/wwwroot/index.html
index ceaaadfa..11dd88e2 100644
--- a/PolyPilot/wwwroot/index.html
+++ b/PolyPilot/wwwroot/index.html
@@ -23,6 +23,7 @@
+