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 @@ +