feat: copy output easily. also build fixes.
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -2,6 +2,7 @@
|
|||||||
"chat.tools.terminal.autoApprove": {
|
"chat.tools.terminal.autoApprove": {
|
||||||
"npx vitest": true,
|
"npx vitest": true,
|
||||||
"npx tsc": true,
|
"npx tsc": true,
|
||||||
"git remote": true
|
"git remote": true,
|
||||||
|
"npx asar": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,31 @@
|
|||||||
gap: 6px;
|
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 {
|
.output-item {
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -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) => (
|
const renderTaskRow = (task: TaskProgress, isChild = false) => (
|
||||||
<div key={task.taskId} className={`task-item status-${task.status} ${isChild ? 'task-child-row' : ''}`.trim()}>
|
<div key={task.taskId} className={`task-item status-${task.status} ${isChild ? 'task-child-row' : ''}`.trim()}>
|
||||||
<div className="task-status">
|
<div className="task-status">
|
||||||
@@ -387,12 +416,25 @@ export const Panel: React.FC = () => {
|
|||||||
panelOutputEntries.length === 0 ? (
|
panelOutputEntries.length === 0 ? (
|
||||||
<div className="panel-empty">{t('panel.noOutput')}</div>
|
<div className="panel-empty">{t('panel.noOutput')}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="output-list">
|
<div className="output-content">
|
||||||
{panelOutputEntries.map((entry) => (
|
<div className="output-toolbar">
|
||||||
<div key={entry.id} className={`output-item output-${entry.kind}`}>
|
<button
|
||||||
{entry.message}
|
type="button"
|
||||||
</div>
|
className="output-copy-button"
|
||||||
))}
|
onClick={() => {
|
||||||
|
void handleCopyOutput();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('panel.copyOutput')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="output-list">
|
||||||
|
{panelOutputEntries.map((entry) => (
|
||||||
|
<div key={entry.id} className={`output-item output-${entry.kind}`}>
|
||||||
|
{entry.message}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -629,6 +629,7 @@
|
|||||||
"panel.closeTitle": "Panel schließen",
|
"panel.closeTitle": "Panel schließen",
|
||||||
"panel.noRecentTasks": "Keine aktuellen Aufgaben",
|
"panel.noRecentTasks": "Keine aktuellen Aufgaben",
|
||||||
"panel.noOutput": "Keine Ausgabe",
|
"panel.noOutput": "Keine Ausgabe",
|
||||||
|
"panel.copyOutput": "Ausgabe kopieren",
|
||||||
"panel.openPostEditor": "Öffne einen Beitragseditor, um Beitragslinks zu sehen",
|
"panel.openPostEditor": "Öffne einen Beitragseditor, um Beitragslinks zu sehen",
|
||||||
"panel.loadingPostLinks": "Beitragslinks werden geladen...",
|
"panel.loadingPostLinks": "Beitragslinks werden geladen...",
|
||||||
"panel.noPostLinks": "Keine Beitragslinks für diesen Beitrag",
|
"panel.noPostLinks": "Keine Beitragslinks für diesen Beitrag",
|
||||||
|
|||||||
@@ -629,6 +629,7 @@
|
|||||||
"panel.closeTitle": "Close panel",
|
"panel.closeTitle": "Close panel",
|
||||||
"panel.noRecentTasks": "No recent tasks",
|
"panel.noRecentTasks": "No recent tasks",
|
||||||
"panel.noOutput": "No output",
|
"panel.noOutput": "No output",
|
||||||
|
"panel.copyOutput": "Copy Output",
|
||||||
"panel.openPostEditor": "Open a post editor to view post links",
|
"panel.openPostEditor": "Open a post editor to view post links",
|
||||||
"panel.loadingPostLinks": "Loading post links...",
|
"panel.loadingPostLinks": "Loading post links...",
|
||||||
"panel.noPostLinks": "No post links for this post",
|
"panel.noPostLinks": "No post links for this post",
|
||||||
|
|||||||
@@ -629,6 +629,7 @@
|
|||||||
"panel.closeTitle": "Cerrar panel",
|
"panel.closeTitle": "Cerrar panel",
|
||||||
"panel.noRecentTasks": "No hay tareas recientes",
|
"panel.noRecentTasks": "No hay tareas recientes",
|
||||||
"panel.noOutput": "Sin salida",
|
"panel.noOutput": "Sin salida",
|
||||||
|
"panel.copyOutput": "Copiar salida",
|
||||||
"panel.openPostEditor": "Abre un editor de entradas para ver los enlaces",
|
"panel.openPostEditor": "Abre un editor de entradas para ver los enlaces",
|
||||||
"panel.loadingPostLinks": "Cargando enlaces de entradas...",
|
"panel.loadingPostLinks": "Cargando enlaces de entradas...",
|
||||||
"panel.noPostLinks": "No hay enlaces para esta entrada",
|
"panel.noPostLinks": "No hay enlaces para esta entrada",
|
||||||
|
|||||||
@@ -629,6 +629,7 @@
|
|||||||
"panel.closeTitle": "Fermer le panneau",
|
"panel.closeTitle": "Fermer le panneau",
|
||||||
"panel.noRecentTasks": "Aucune tâche récente",
|
"panel.noRecentTasks": "Aucune tâche récente",
|
||||||
"panel.noOutput": "Aucune sortie",
|
"panel.noOutput": "Aucune sortie",
|
||||||
|
"panel.copyOutput": "Copier la sortie",
|
||||||
"panel.openPostEditor": "Ouvrez un éditeur d'article pour voir les liens",
|
"panel.openPostEditor": "Ouvrez un éditeur d'article pour voir les liens",
|
||||||
"panel.loadingPostLinks": "Chargement des liens d'articles...",
|
"panel.loadingPostLinks": "Chargement des liens d'articles...",
|
||||||
"panel.noPostLinks": "Aucun lien pour cet article",
|
"panel.noPostLinks": "Aucun lien pour cet article",
|
||||||
|
|||||||
@@ -629,6 +629,7 @@
|
|||||||
"panel.closeTitle": "Chiudi pannello",
|
"panel.closeTitle": "Chiudi pannello",
|
||||||
"panel.noRecentTasks": "Nessuna attività recente",
|
"panel.noRecentTasks": "Nessuna attività recente",
|
||||||
"panel.noOutput": "Nessun output",
|
"panel.noOutput": "Nessun output",
|
||||||
|
"panel.copyOutput": "Copia output",
|
||||||
"panel.openPostEditor": "Apri un editor post per visualizzare i collegamenti",
|
"panel.openPostEditor": "Apri un editor post per visualizzare i collegamenti",
|
||||||
"panel.loadingPostLinks": "Caricamento collegamenti post...",
|
"panel.loadingPostLinks": "Caricamento collegamenti post...",
|
||||||
"panel.noPostLinks": "Nessun collegamento per questo post",
|
"panel.noPostLinks": "Nessun collegamento per questo post",
|
||||||
|
|||||||
3
src/renderer/python/pyodideAssetUrl.ts
Normal file
3
src/renderer/python/pyodideAssetUrl.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function resolvePyodideIndexURL(workerModuleUrl: string): string {
|
||||||
|
return new URL('../../../node_modules/pyodide/', workerModuleUrl).toString();
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { loadPyodide, type PyodideInterface } from 'pyodide';
|
import { loadPyodide, type PyodideInterface } from 'pyodide';
|
||||||
import type { PythonWorkerMessage, PythonWorkerRequest } from './runtimeProtocol';
|
import type { PythonWorkerMessage, PythonWorkerRequest } from './runtimeProtocol';
|
||||||
import { parseMacroContextV1, parseMacroResultV1 } from './abiV1';
|
import { parseMacroContextV1, parseMacroResultV1 } from './abiV1';
|
||||||
|
import { resolvePyodideIndexURL } from './pyodideAssetUrl';
|
||||||
|
|
||||||
let runtime: PyodideInterface | null = null;
|
let runtime: PyodideInterface | null = null;
|
||||||
let activeRequestId: string | null = null;
|
let activeRequestId: string | null = null;
|
||||||
@@ -183,6 +184,7 @@ json.dumps(__bds_entrypoints)
|
|||||||
async function bootstrapRuntime(): Promise<void> {
|
async function bootstrapRuntime(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
runtime = await loadPyodide({
|
runtime = await loadPyodide({
|
||||||
|
indexURL: resolvePyodideIndexURL(import.meta.url),
|
||||||
stdout: (chunk) => {
|
stdout: (chunk) => {
|
||||||
if (!activeRequestId) {
|
if (!activeRequestId) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -253,6 +253,40 @@ describe('Panel', () => {
|
|||||||
expect(screen.getByText('hello from script')).toBeInTheDocument();
|
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(<Panel />);
|
||||||
|
|
||||||
|
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 () => {
|
it('renders grouped tasks as expandable parent rows with child task names in tasks tab', async () => {
|
||||||
useAppStore.setState({
|
useAppStore.setState({
|
||||||
tasks: [
|
tasks: [
|
||||||
|
|||||||
16
tests/renderer/python/pyodideAssetUrl.test.ts
Normal file
16
tests/renderer/python/pyodideAssetUrl.test.ts
Normal file
@@ -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/');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user