From bf945716f971a3bb1aaa6aaf72799696c1dd6608 Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 23 Feb 2026 12:07:19 +0100 Subject: [PATCH] feat: copy output easily. also build fixes. --- .vscode/settings.json | 3 +- src/renderer/components/Panel/Panel.css | 25 +++++++++ src/renderer/components/Panel/Panel.tsx | 54 ++++++++++++++++--- src/renderer/i18n/locales/de.json | 1 + src/renderer/i18n/locales/en.json | 1 + src/renderer/i18n/locales/es.json | 1 + src/renderer/i18n/locales/fr.json | 1 + src/renderer/i18n/locales/it.json | 1 + src/renderer/python/pyodideAssetUrl.ts | 3 ++ src/renderer/python/pythonRuntime.worker.ts | 2 + tests/renderer/components/Panel.test.tsx | 34 ++++++++++++ tests/renderer/python/pyodideAssetUrl.test.ts | 16 ++++++ 12 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 src/renderer/python/pyodideAssetUrl.ts create mode 100644 tests/renderer/python/pyodideAssetUrl.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 47a447a..6cadd54 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "chat.tools.terminal.autoApprove": { "npx vitest": true, "npx tsc": true, - "git remote": true + "git remote": true, + "npx asar": true } } \ No newline at end of file diff --git a/src/renderer/components/Panel/Panel.css b/src/renderer/components/Panel/Panel.css index 9c0ba5a..2c069e2 100644 --- a/src/renderer/components/Panel/Panel.css +++ b/src/renderer/components/Panel/Panel.css @@ -92,6 +92,31 @@ gap: 6px; } +.output-content { + display: flex; + flex-direction: column; + gap: 8px; +} + +.output-toolbar { + display: flex; + justify-content: flex-end; +} + +.output-copy-button { + font-size: 11px; + padding: 3px 10px; + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + cursor: pointer; +} + +.output-copy-button:hover { + background-color: var(--vscode-list-hoverBackground); +} + .output-item { background-color: var(--vscode-sideBar-background); border-radius: 4px; diff --git a/src/renderer/components/Panel/Panel.tsx b/src/renderer/components/Panel/Panel.tsx index c2a105c..593fc72 100644 --- a/src/renderer/components/Panel/Panel.tsx +++ b/src/renderer/components/Panel/Panel.tsx @@ -250,6 +250,35 @@ export const Panel: React.FC = () => { }); }; + const handleCopyOutput = async () => { + if (panelOutputEntries.length === 0) { + return; + } + + const outputText = panelOutputEntries.map((entry) => entry.message).join('\n\n'); + + if (typeof navigator !== 'undefined' && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { + await navigator.clipboard.writeText(outputText); + return; + } + + if (typeof document === 'undefined' || typeof document.createElement !== 'function') { + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = outputText; + textArea.setAttribute('readonly', ''); + textArea.style.position = 'absolute'; + textArea.style.left = '-9999px'; + document.body.appendChild(textArea); + textArea.select(); + if (typeof document.execCommand === 'function') { + document.execCommand('copy'); + } + document.body.removeChild(textArea); + }; + const renderTaskRow = (task: TaskProgress, isChild = false) => (
@@ -387,12 +416,25 @@ export const Panel: React.FC = () => { panelOutputEntries.length === 0 ? (
{t('panel.noOutput')}
) : ( -
- {panelOutputEntries.map((entry) => ( -
- {entry.message} -
- ))} +
+
+ +
+
+ {panelOutputEntries.map((entry) => ( +
+ {entry.message} +
+ ))} +
) )} diff --git a/src/renderer/i18n/locales/de.json b/src/renderer/i18n/locales/de.json index 1a2ea61..1d9a738 100644 --- a/src/renderer/i18n/locales/de.json +++ b/src/renderer/i18n/locales/de.json @@ -629,6 +629,7 @@ "panel.closeTitle": "Panel schließen", "panel.noRecentTasks": "Keine aktuellen Aufgaben", "panel.noOutput": "Keine Ausgabe", + "panel.copyOutput": "Ausgabe kopieren", "panel.openPostEditor": "Öffne einen Beitragseditor, um Beitragslinks zu sehen", "panel.loadingPostLinks": "Beitragslinks werden geladen...", "panel.noPostLinks": "Keine Beitragslinks für diesen Beitrag", diff --git a/src/renderer/i18n/locales/en.json b/src/renderer/i18n/locales/en.json index dc743e5..7263b00 100644 --- a/src/renderer/i18n/locales/en.json +++ b/src/renderer/i18n/locales/en.json @@ -629,6 +629,7 @@ "panel.closeTitle": "Close panel", "panel.noRecentTasks": "No recent tasks", "panel.noOutput": "No output", + "panel.copyOutput": "Copy Output", "panel.openPostEditor": "Open a post editor to view post links", "panel.loadingPostLinks": "Loading post links...", "panel.noPostLinks": "No post links for this post", diff --git a/src/renderer/i18n/locales/es.json b/src/renderer/i18n/locales/es.json index 17b2b5a..eaccd78 100644 --- a/src/renderer/i18n/locales/es.json +++ b/src/renderer/i18n/locales/es.json @@ -629,6 +629,7 @@ "panel.closeTitle": "Cerrar panel", "panel.noRecentTasks": "No hay tareas recientes", "panel.noOutput": "Sin salida", + "panel.copyOutput": "Copiar salida", "panel.openPostEditor": "Abre un editor de entradas para ver los enlaces", "panel.loadingPostLinks": "Cargando enlaces de entradas...", "panel.noPostLinks": "No hay enlaces para esta entrada", diff --git a/src/renderer/i18n/locales/fr.json b/src/renderer/i18n/locales/fr.json index cf421fd..6201684 100644 --- a/src/renderer/i18n/locales/fr.json +++ b/src/renderer/i18n/locales/fr.json @@ -629,6 +629,7 @@ "panel.closeTitle": "Fermer le panneau", "panel.noRecentTasks": "Aucune tâche récente", "panel.noOutput": "Aucune sortie", + "panel.copyOutput": "Copier la sortie", "panel.openPostEditor": "Ouvrez un éditeur d'article pour voir les liens", "panel.loadingPostLinks": "Chargement des liens d'articles...", "panel.noPostLinks": "Aucun lien pour cet article", diff --git a/src/renderer/i18n/locales/it.json b/src/renderer/i18n/locales/it.json index 6894fb8..021a194 100644 --- a/src/renderer/i18n/locales/it.json +++ b/src/renderer/i18n/locales/it.json @@ -629,6 +629,7 @@ "panel.closeTitle": "Chiudi pannello", "panel.noRecentTasks": "Nessuna attività recente", "panel.noOutput": "Nessun output", + "panel.copyOutput": "Copia output", "panel.openPostEditor": "Apri un editor post per visualizzare i collegamenti", "panel.loadingPostLinks": "Caricamento collegamenti post...", "panel.noPostLinks": "Nessun collegamento per questo post", diff --git a/src/renderer/python/pyodideAssetUrl.ts b/src/renderer/python/pyodideAssetUrl.ts new file mode 100644 index 0000000..5b243e5 --- /dev/null +++ b/src/renderer/python/pyodideAssetUrl.ts @@ -0,0 +1,3 @@ +export function resolvePyodideIndexURL(workerModuleUrl: string): string { + return new URL('../../../node_modules/pyodide/', workerModuleUrl).toString(); +} diff --git a/src/renderer/python/pythonRuntime.worker.ts b/src/renderer/python/pythonRuntime.worker.ts index 8582524..6ff43e6 100644 --- a/src/renderer/python/pythonRuntime.worker.ts +++ b/src/renderer/python/pythonRuntime.worker.ts @@ -1,6 +1,7 @@ import { loadPyodide, type PyodideInterface } from 'pyodide'; import type { PythonWorkerMessage, PythonWorkerRequest } from './runtimeProtocol'; import { parseMacroContextV1, parseMacroResultV1 } from './abiV1'; +import { resolvePyodideIndexURL } from './pyodideAssetUrl'; let runtime: PyodideInterface | null = null; let activeRequestId: string | null = null; @@ -183,6 +184,7 @@ json.dumps(__bds_entrypoints) async function bootstrapRuntime(): Promise { try { runtime = await loadPyodide({ + indexURL: resolvePyodideIndexURL(import.meta.url), stdout: (chunk) => { if (!activeRequestId) { return; diff --git a/tests/renderer/components/Panel.test.tsx b/tests/renderer/components/Panel.test.tsx index 87b5e2e..ee1187a 100644 --- a/tests/renderer/components/Panel.test.tsx +++ b/tests/renderer/components/Panel.test.tsx @@ -253,6 +253,40 @@ describe('Panel', () => { expect(screen.getByText('hello from script')).toBeInTheDocument(); }); + it('copies output entries from output tab with copy button', async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(globalThis, 'navigator', { + value: { clipboard: { writeText } }, + configurable: true, + }); + + useAppStore.setState({ + panelActiveTab: 'output', + panelOutputEntries: [ + { + id: 'output-1', + message: 'pyodide.asm.js missing', + createdAt: '2026-02-22T00:00:00.000Z', + kind: 'error', + }, + { + id: 'output-2', + message: 'second line', + createdAt: '2026-02-22T00:00:01.000Z', + kind: 'stdout', + }, + ], + }); + + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Copy Output' })); + + await vi.waitFor(() => { + expect(writeText).toHaveBeenCalledWith('pyodide.asm.js missing\n\nsecond line'); + }); + }); + it('renders grouped tasks as expandable parent rows with child task names in tasks tab', async () => { useAppStore.setState({ tasks: [ diff --git a/tests/renderer/python/pyodideAssetUrl.test.ts b/tests/renderer/python/pyodideAssetUrl.test.ts new file mode 100644 index 0000000..3fc99f1 --- /dev/null +++ b/tests/renderer/python/pyodideAssetUrl.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { resolvePyodideIndexURL } from '../../../src/renderer/python/pyodideAssetUrl'; + +describe('resolvePyodideIndexURL', () => { + it('resolves to packaged node_modules path for dist worker urls', () => { + const workerUrl = 'file:///Applications/bDS.app/Contents/Resources/app.asar/dist/renderer/assets/pythonRuntime.worker-abc123.js'; + expect(resolvePyodideIndexURL(workerUrl)).toBe( + 'file:///Applications/bDS.app/Contents/Resources/app.asar/node_modules/pyodide/' + ); + }); + + it('resolves to vite node_modules path for dev worker urls', () => { + const workerUrl = 'http://localhost:5173/src/renderer/python/pythonRuntime.worker.ts'; + expect(resolvePyodideIndexURL(workerUrl)).toBe('http://localhost:5173/node_modules/pyodide/'); + }); +});