From 550a83b0bd927d53a38db5fd7dd2bbcd2c5259d0 Mon Sep 17 00:00:00 2001 From: Lexus2016 Date: Tue, 10 Nov 2026 18:19:42 +0000 Subject: [PATCH] file viewer: a Copy action for the previewed file (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The preview toolbar offered Download, Open and Share but no way to take the file's content, so copying meant opening the file elsewhere or dragging through the pane by hand. The Copy button appears only once the viewer has actually loaded text, and copies exactly the string that was fetched — the pane itself is escaped HTML. It also works for a remote project: the text is already in memory, so it never reaches /api/files/raw or /api/files/download, which is what issue #57 refuses and why Download and Share stay hidden there. Strings are added for all five languages the SPA ships. --- package.json | 2 +- public/index.html | 51 ++++++++--- test/file-preview-copy.test.js | 149 +++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 test/file-preview-copy.test.js diff --git a/package.json b/package.json index 4fd7ecd..bfc9068 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "start": "node server.js", "dev": "node --watch server.js", - "test": "node --test test/render/*.test.mjs && node test/overload-detector.test.js && node test/usage-limit.test.js && node test/auth-errors.test.js && node test/env-load-order.test.js && node test/multi-agent-result.test.js && node test/terminal-session.test.js && node test/terminal-schema.test.js && node test/bots.test.js && node test/bot-inbox.test.js && node test/bots-api.test.js && node test/running-sessions.test.js && node test/session-liveness.test.js && node test/terminal-bridge.integration.test.js && node test/tmux-composite.test.js && node test/engine-spawn-cmd.test.js && node test/claude-firstrun.test.js && node test/telegram-format.test.js && node test/telegram-behaviour.test.js && node test/ask-user-question.test.js && node test/delegate-terminal.test.js && node test/agent-catalog.test.js && node test/update-flow.test.js && node test/kanban-schedule.test.js && node test/kanban-run-badges.test.js && node test/task-backlog.test.js && node test/i18n-completeness.test.js && node test/config-resolve.test.js && node test/chat-defaults.test.js && node test/chat-defaults-api.test.js && node test/run-continuation.test.js && node test/worktree-manager.test.js && node test/session-expiry.test.js && node test/setup-gate.test.js && node test/path-guard.test.js && node test/subscribe-nocatchup.test.js && node test/cli-import-remote.test.js && node test/remote-list-parse.test.js && node test/config-migrate.test.js && node test/engine-pane.test.js && node test/global-workspace.test.js && node test/ssh-secret.test.js && node test/agent-dag.test.js && node test/ssh-parser.test.js && node test/ssh-termination.test.js && node test/session-restart.test.js && node test/remote-env.test.js && node test/remote-files.test.js && node test/remote-files-api.test.js && node test/auth-token.test.js && node test/cmd-model.test.js && node test/origin-guard.test.js && node test/interrupted-recovery.test.js && node test/queue-persistence.test.js && node test/interrupt-idle-config.test.js && node test/agents-md.test.js && node test/editor-links.test.js && node test/editor-open-api.test.js && node test/interrupt-delivery.test.js && node test/terminal-pane-guard.integration.test.js && node test/composer-terminal.test.js && node test/claude-cli-status.test.js && node test/asar-helpers.test.js && node test/mac-signing.test.js && node test/release-mac.test.js && node test/task-workers.test.js && node test/git-clone.test.js && node test/responsive-ui.test.js && node test/kanban-delegation.test.js && node test/kanban-group-button.test.js && node test/terminal-keys.test.js && node test/electron-navigation.test.js && node test/cli-runtime.test.js && node test/worktree-safety.test.js && node test/auth-boundary.test.js && node test/cli-process-tree.test.js && node test/schedule-actions.test.js && node test/install-hooks.test.js && node test/frontend-recovery.test.js && node test/db-revisions.test.js && node test/frontend-refresh-races.test.js && node test/release-gate.test.js && node test/focused-workspace.test.js && node test/secondary-navigation.test.js && node test/section-controls-ux.test.js && node test/settings-ux.test.js && node test/team-forms-ux.test.js && node test/page-forms-ux.test.js && node test/sidebar-ux.test.js && node test/task-text-limit.test.js && node test/terminal-clipboard.test.js && node test/terminal-addons.test.js && node test/telegram-lang.test.js", + "test": "node --test test/render/*.test.mjs && node test/overload-detector.test.js && node test/usage-limit.test.js && node test/auth-errors.test.js && node test/env-load-order.test.js && node test/multi-agent-result.test.js && node test/terminal-session.test.js && node test/terminal-schema.test.js && node test/bots.test.js && node test/bot-inbox.test.js && node test/bots-api.test.js && node test/running-sessions.test.js && node test/session-liveness.test.js && node test/terminal-bridge.integration.test.js && node test/tmux-composite.test.js && node test/engine-spawn-cmd.test.js && node test/claude-firstrun.test.js && node test/telegram-format.test.js && node test/telegram-behaviour.test.js && node test/ask-user-question.test.js && node test/delegate-terminal.test.js && node test/agent-catalog.test.js && node test/update-flow.test.js && node test/kanban-schedule.test.js && node test/kanban-run-badges.test.js && node test/task-backlog.test.js && node test/i18n-completeness.test.js && node test/config-resolve.test.js && node test/chat-defaults.test.js && node test/chat-defaults-api.test.js && node test/run-continuation.test.js && node test/worktree-manager.test.js && node test/session-expiry.test.js && node test/setup-gate.test.js && node test/path-guard.test.js && node test/subscribe-nocatchup.test.js && node test/cli-import-remote.test.js && node test/remote-list-parse.test.js && node test/config-migrate.test.js && node test/engine-pane.test.js && node test/global-workspace.test.js && node test/ssh-secret.test.js && node test/agent-dag.test.js && node test/ssh-parser.test.js && node test/ssh-termination.test.js && node test/session-restart.test.js && node test/remote-env.test.js && node test/remote-files.test.js && node test/remote-files-api.test.js && node test/auth-token.test.js && node test/cmd-model.test.js && node test/origin-guard.test.js && node test/interrupted-recovery.test.js && node test/queue-persistence.test.js && node test/interrupt-idle-config.test.js && node test/agents-md.test.js && node test/editor-links.test.js && node test/editor-open-api.test.js && node test/interrupt-delivery.test.js && node test/terminal-pane-guard.integration.test.js && node test/composer-terminal.test.js && node test/claude-cli-status.test.js && node test/asar-helpers.test.js && node test/mac-signing.test.js && node test/release-mac.test.js && node test/task-workers.test.js && node test/git-clone.test.js && node test/responsive-ui.test.js && node test/kanban-delegation.test.js && node test/kanban-group-button.test.js && node test/terminal-keys.test.js && node test/electron-navigation.test.js && node test/cli-runtime.test.js && node test/worktree-safety.test.js && node test/auth-boundary.test.js && node test/cli-process-tree.test.js && node test/schedule-actions.test.js && node test/install-hooks.test.js && node test/frontend-recovery.test.js && node test/db-revisions.test.js && node test/frontend-refresh-races.test.js && node test/release-gate.test.js && node test/focused-workspace.test.js && node test/secondary-navigation.test.js && node test/section-controls-ux.test.js && node test/settings-ux.test.js && node test/team-forms-ux.test.js && node test/page-forms-ux.test.js && node test/sidebar-ux.test.js && node test/task-text-limit.test.js && node test/terminal-clipboard.test.js && node test/terminal-addons.test.js && node test/file-preview-copy.test.js && node test/telegram-lang.test.js", "postinstall": "node scripts/install-hooks.js", "release": "node scripts/release.js", "release:mac": "node scripts/release-mac.js", diff --git a/public/index.html b/public/index.html index 9d1ce3e..638be38 100644 --- a/public/index.html +++ b/public/index.html @@ -4175,6 +4175,10 @@

Copy + `; return; } + // Only the text and markdown paths reach this point with content worth copying: a + // binary, image or PDF preview returned above with the button still hidden. + _fpvText = d.content || ''; + $i('fpvCopyBtn').style.display = _fpvText ? '' : 'none'; if (mdExts.includes(ext)) { body.innerHTML = `
${renderMd(d.content || '')}
`; } else { @@ -16099,6 +16112,20 @@

Delegate to Agent

if (_fpvPath && !_filesRemote) downloadFile(_fpvPath); } +// Copy the text the viewer already loaded (issue #137). Works for a remote project too: +// the text is in memory, so neither /api/files/raw nor /api/files/download is involved — +// those are what issue #57 refuses, and why Download and Share are hidden for remote. +async function fpvCopyText() { + if (!_fpvPath || !_fpvText) return; + try { + await copyText(_fpvText); + toast(t('toast.text_copied')); + } catch (e) { + console.error('Failed to copy file contents:', e); + toast(t('toast.err_prefix') + ' ' + t('fpv.copy')); + } +} + async function fpvShare() { if (!_fpvPath || _filesRemote) return; const url = location.origin + `/api/files/raw?${_filesQS(_fpvPath)}`; diff --git a/test/file-preview-copy.test.js b/test/file-preview-copy.test.js new file mode 100644 index 0000000..1ab3d10 --- /dev/null +++ b/test/file-preview-copy.test.js @@ -0,0 +1,149 @@ +'use strict'; +// Issue #137 — a Copy action in the file preview viewer. +// +// The viewer's functions are RUN, not regex-matched: the file-preview block is cut out of +// public/index.html and evaluated in a vm with a stubbed fetch, $i and clipboard. What must +// hold: +// - the toolbar carries a Copy button that is hidden until there is text to copy; +// - a text or markdown preview shows it and copies exactly the content that was fetched, +// not the escaped DOM; +// - a binary, image or PDF preview leaves it hidden, and an empty file has nothing to copy; +// - closing the viewer hides it and clears the stored text, so a stale path cannot be copied; +// - the copy path is the shared copyText(), which also works over plain http. +// +// Run: node test/file-preview-copy.test.js +const assert = require('assert'); +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); + +let pass = 0, fail = 0; +function check(label, actual, expected) { + try { assert.deepStrictEqual(actual, expected); pass++; console.log(` ok ${label}`); } + catch { fail++; console.error(` FAIL ${label} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); } +} + +const html = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8'); + +console.log('toolbar:'); +{ + const btn = html.match(/