From a1ba14e872f6832c8431bda5b74e54d9bb1bd44f Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Fri, 5 Dec 2025 06:31:19 -0800 Subject: [PATCH 1/3] fix: Enable clipboard copy/paste in dashboard terminals Fixes GitHub issue #42. Root cause: ttyd terminals embedded in iframes lacked clipboard permissions. Changes: 1. Add allow="clipboard-read; clipboard-write" to all iframe elements in the dashboard templates to grant clipboard access via Permissions Policy 2. Add rightClickSelectsWord=true option to ttyd for better UX - this is standard behavior in macOS applications Files changed: - codev/templates/dashboard-split.html (iframe permissions) - codev-skeleton/templates/dashboard-split.html (iframe permissions) - agent-farm/src/commands/start.ts (ttyd option) - agent-farm/src/commands/util.ts (ttyd option) - agent-farm/src/servers/dashboard-server.ts (ttyd option) Resolves: #42 --- agent-farm/src/commands/start.ts | 1 + agent-farm/src/commands/util.ts | 1 + agent-farm/src/servers/dashboard-server.ts | 1 + codev-skeleton/templates/dashboard-split.html | 4 ++-- codev/templates/dashboard-split.html | 4 ++-- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/agent-farm/src/commands/start.ts b/agent-farm/src/commands/start.ts index 6d22b11d5..a908c4ff3 100644 --- a/agent-farm/src/commands/start.ts +++ b/agent-farm/src/commands/start.ts @@ -117,6 +117,7 @@ export async function start(options: StartOptions = {}): Promise { '-W', '-p', String(architectPort), '-t', 'theme={"background":"#000000"}', + '-t', 'rightClickSelectsWord=true', // Enable word selection on right-click for better UX ]; // Add custom index if it exists diff --git a/agent-farm/src/commands/util.ts b/agent-farm/src/commands/util.ts index 3c7ace247..81c71eebc 100644 --- a/agent-farm/src/commands/util.ts +++ b/agent-farm/src/commands/util.ts @@ -92,6 +92,7 @@ export async function util(options: UtilOptions = {}): Promise { '-p', String(port), '-t', `titleFixed=${name}`, '-t', 'fontSize=14', + '-t', 'rightClickSelectsWord=true', // Enable word selection on right-click for better UX shell, ]; diff --git a/agent-farm/src/servers/dashboard-server.ts b/agent-farm/src/servers/dashboard-server.ts index 296f72bee..705a35373 100644 --- a/agent-farm/src/servers/dashboard-server.ts +++ b/agent-farm/src/servers/dashboard-server.ts @@ -343,6 +343,7 @@ function spawnTmuxWithTtyd( '-p', String(ttydPort), '-t', 'theme={"background":"#000000"}', '-t', 'fontSize=14', + '-t', 'rightClickSelectsWord=true', // Enable word selection on right-click for better UX ]; // Add custom index if it exists diff --git a/codev-skeleton/templates/dashboard-split.html b/codev-skeleton/templates/dashboard-split.html index e18a4c93e..557da6672 100644 --- a/codev-skeleton/templates/dashboard-split.html +++ b/codev-skeleton/templates/dashboard-split.html @@ -781,7 +781,7 @@

Close tab?

// Only update iframe if port changed (avoid flashing on poll) if (currentArchitectPort !== state.architect.port) { currentArchitectPort = state.architect.port; - content.innerHTML = ``; + content.innerHTML = ``; } } else { if (currentArchitectPort !== null) { @@ -895,7 +895,7 @@

Close tab?

// Only update iframe if port changed (avoid flashing on poll) if (currentTabPort !== tab.port) { currentTabPort = tab.port; - content.innerHTML = ``; + content.innerHTML = ``; } } diff --git a/codev/templates/dashboard-split.html b/codev/templates/dashboard-split.html index e18a4c93e..557da6672 100644 --- a/codev/templates/dashboard-split.html +++ b/codev/templates/dashboard-split.html @@ -781,7 +781,7 @@

Close tab?

// Only update iframe if port changed (avoid flashing on poll) if (currentArchitectPort !== state.architect.port) { currentArchitectPort = state.architect.port; - content.innerHTML = ``; + content.innerHTML = ``; } } else { if (currentArchitectPort !== null) { @@ -895,7 +895,7 @@

Close tab?

// Only update iframe if port changed (avoid flashing on poll) if (currentTabPort !== tab.port) { currentTabPort = tab.port; - content.innerHTML = ``; + content.innerHTML = ``; } } From f6f87c0abb3972511cf5a8ef6edfbde2e793fa69 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Fri, 5 Dec 2025 06:47:40 -0800 Subject: [PATCH 2/3] fix: Complete clipboard fix for all templates and spawn points - Add clipboard permissions to non-split dashboard.html (codev + skeleton) - Add rightClickSelectsWord to spawn.ts builder/shell/worktree functions Addresses review feedback on PR #43 --- agent-farm/src/commands/spawn.ts | 3 +++ codev-skeleton/templates/dashboard.html | 4 ++-- codev/templates/dashboard.html | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/agent-farm/src/commands/spawn.ts b/agent-farm/src/commands/spawn.ts index 8702430c1..92c372e6f 100644 --- a/agent-farm/src/commands/spawn.ts +++ b/agent-farm/src/commands/spawn.ts @@ -251,6 +251,7 @@ async function startBuilderSession( '-W', '-p', String(port), '-t', 'theme={"background":"#000000"}', + '-t', 'rightClickSelectsWord=true', // Enable word selection on right-click for better UX ]; if (existsSync(customIndexPath)) { @@ -297,6 +298,7 @@ async function startShellSession( '-W', '-p', String(port), '-t', 'theme={"background":"#000000"}', + '-t', 'rightClickSelectsWord=true', // Enable word selection on right-click for better UX ]; if (existsSync(customIndexPath)) { @@ -611,6 +613,7 @@ async function spawnWorktree(options: SpawnOptions, config: Config): PromiseArchitect Console

Architect

active - + `; @@ -119,7 +119,7 @@

Builders (${builders.length})

Builder ${b.id}: ${b.name}

${b.status} (${b.phase}) - + `; }).join('')} diff --git a/codev/templates/dashboard.html b/codev/templates/dashboard.html index 1c36e2d25..ebca683d2 100644 --- a/codev/templates/dashboard.html +++ b/codev/templates/dashboard.html @@ -88,7 +88,7 @@

Architect Console

Architect

active - + `; @@ -119,7 +119,7 @@

Builders (${builders.length})

Builder ${b.id}: ${b.name}

${b.status} (${b.phase}) - + `; }).join('')} From a52fbaad6d1cccdd54b312534c44bb51fe5a24d8 Mon Sep 17 00:00:00 2001 From: M Waleed Kadous Date: Fri, 5 Dec 2025 08:38:37 -0800 Subject: [PATCH 3/3] test: Add regression tests for clipboard functionality - Test that all dashboard HTML templates include clipboard permissions - Test that all ttyd spawn locations include rightClickSelectsWord - Prevents regression of issue #42 fix 9 new tests added, all passing (44 total) --- agent-farm/src/__tests__/clipboard.test.ts | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 agent-farm/src/__tests__/clipboard.test.ts diff --git a/agent-farm/src/__tests__/clipboard.test.ts b/agent-farm/src/__tests__/clipboard.test.ts new file mode 100644 index 000000000..b53228f3b --- /dev/null +++ b/agent-farm/src/__tests__/clipboard.test.ts @@ -0,0 +1,101 @@ +/** + * Tests for clipboard functionality in dashboard terminals + * Ensures iframes have clipboard permissions and ttyd has rightClickSelectsWord + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; + +// Find project root by looking for codev directory +function findProjectRoot(): string { + let dir = process.cwd(); + while (dir !== '/') { + if (existsSync(resolve(dir, 'codev'))) { + return dir; + } + dir = resolve(dir, '..'); + } + return process.cwd(); +} + +const projectRoot = findProjectRoot(); + +describe('Dashboard clipboard permissions', () => { + const templates = [ + 'codev/templates/dashboard-split.html', + 'codev/templates/dashboard.html', + 'codev-skeleton/templates/dashboard-split.html', + 'codev-skeleton/templates/dashboard.html', + ]; + + templates.forEach((templatePath) => { + it(`${templatePath} includes clipboard permissions on iframes`, () => { + const fullPath = resolve(projectRoot, templatePath); + + if (!existsSync(fullPath)) { + // Skip if template doesn't exist (might be in a worktree without skeleton) + return; + } + + const content = readFileSync(fullPath, 'utf-8'); + + // Find all iframe tags + const iframeRegex = /]*>/g; + const iframes = content.match(iframeRegex) || []; + + expect(iframes.length).toBeGreaterThan(0); + + // Each iframe should have clipboard permissions + iframes.forEach((iframe) => { + expect(iframe).toMatch(/allow="[^"]*clipboard-read[^"]*"/); + expect(iframe).toMatch(/allow="[^"]*clipboard-write[^"]*"/); + }); + }); + }); +}); + +describe('ttyd rightClickSelectsWord option', () => { + const sourceFiles = [ + 'agent-farm/src/commands/start.ts', + 'agent-farm/src/commands/util.ts', + 'agent-farm/src/commands/spawn.ts', + 'agent-farm/src/servers/dashboard-server.ts', + ]; + + sourceFiles.forEach((filePath) => { + it(`${filePath} includes rightClickSelectsWord in ttyd args`, () => { + const fullPath = resolve(projectRoot, filePath); + + if (!existsSync(fullPath)) { + return; + } + + const content = readFileSync(fullPath, 'utf-8'); + + // Check if file spawns ttyd (has ttydArgs) + if (content.includes('ttydArgs')) { + expect(content).toMatch(/rightClickSelectsWord['"=]?.*true/); + } + }); + }); + + it('spawn.ts has rightClickSelectsWord in all ttyd spawn locations', () => { + const spawnPath = resolve(projectRoot, 'agent-farm/src/commands/spawn.ts'); + + if (!existsSync(spawnPath)) { + return; + } + + const content = readFileSync(spawnPath, 'utf-8'); + + // Count ttydArgs definitions + const ttydArgsCount = (content.match(/const ttydArgs = \[/g) || []).length; + + // Count rightClickSelectsWord occurrences + const rightClickCount = (content.match(/rightClickSelectsWord/g) || []).length; + + // Each ttydArgs should have rightClickSelectsWord + expect(rightClickCount).toBeGreaterThanOrEqual(ttydArgsCount); + }); +});