Merge pull request #68 from aakhter/feat/clipboard-api

feat: add clipboard API for remote browser clipboard access
This commit is contained in:
Ark0N
2026-04-18 04:10:31 +02:00
committed by GitHub
7 changed files with 105 additions and 0 deletions
+3
View File
@@ -249,6 +249,9 @@ const _SSE_HANDLER_MAP = [
[SSE_EVENTS.ORCHESTRATOR_TASK_FAILED, '_onOrchestratorTaskFailed'],
[SSE_EVENTS.ORCHESTRATOR_COMPLETED, '_onOrchestratorCompleted'],
[SSE_EVENTS.ORCHESTRATOR_ERROR, '_onOrchestratorError'],
// Clipboard
[SSE_EVENTS.CLIPBOARD_WRITE, '_onClipboardWrite'],
];
+3
View File
@@ -292,6 +292,9 @@ const SSE_EVENTS = {
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
ORCHESTRATOR_ERROR: 'orchestrator:error',
// Clipboard
CLIPBOARD_WRITE: 'clipboard:write',
// Cases
CASE_CREATED: 'case:created',
CASE_LINKED: 'case:linked',
+64
View File
@@ -3246,4 +3246,68 @@ Object.assign(CodemanApp.prototype, {
}
}
},
// ─── Clipboard ──────────────────────────────────────────────────────────────
async _onClipboardWrite(data) {
const text = data?.text;
if (typeof text !== 'string') return;
try {
await navigator.clipboard.writeText(text);
this.showToast(`Copied to clipboard (${text.length} chars)`, 'success');
} catch {
this._showClipboardFallback(text);
}
},
_showClipboardFallback(text) {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center';
const modal = document.createElement('div');
modal.style.cssText = 'background:#1e1e2e;border:1px solid #444;border-radius:8px;padding:16px;max-width:600px;width:90%;max-height:60vh;display:flex;flex-direction:column;gap:12px';
const header = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center';
const title = document.createElement('span');
title.style.cssText = 'color:#cdd6f4;font-weight:600';
title.textContent = 'Clipboard (browser blocked auto-copy)';
const closeBtn = document.createElement('button');
closeBtn.style.cssText = 'background:none;border:none;color:#cdd6f4;font-size:18px;cursor:pointer';
closeBtn.textContent = '\u00d7';
header.appendChild(title);
header.appendChild(closeBtn);
const textarea = document.createElement('textarea');
textarea.readOnly = true;
textarea.style.cssText = 'background:#181825;color:#cdd6f4;border:1px solid #555;border-radius:4px;padding:8px;font-family:monospace;font-size:13px;resize:none;height:200px;width:100%';
textarea.value = text;
const copyBtn = document.createElement('button');
copyBtn.style.cssText = 'background:#89b4fa;color:#1e1e2e;border:none;border-radius:4px;padding:8px 16px;cursor:pointer;font-weight:600';
copyBtn.textContent = 'Copy to Clipboard';
modal.appendChild(header);
modal.appendChild(textarea);
modal.appendChild(copyBtn);
overlay.appendChild(modal);
document.body.appendChild(overlay);
copyBtn.onclick = async () => {
try {
await navigator.clipboard.writeText(text);
this.showToast('Copied to clipboard', 'success');
overlay.remove();
} catch {
textarea.select();
document.execCommand('copy');
this.showToast('Copied (fallback)', 'success');
overlay.remove();
}
};
const close = () => overlay.remove();
closeBtn.onclick = close;
overlay.onclick = (e) => { if (e.target === overlay) close(); };
},
});
+24
View File
@@ -0,0 +1,24 @@
/**
* @fileoverview Clipboard routes.
* Accepts text via POST and broadcasts to connected browsers for clipboard write.
*/
import { FastifyInstance } from 'fastify';
import { SseEvent } from '../sse-events.js';
import type { EventPort } from '../ports/index.js';
export function registerClipboardRoutes(app: FastifyInstance, ctx: EventPort): void {
app.post('/api/clipboard', async (req) => {
const body = req.body as { text?: string; sessionId?: string };
const text = body?.text;
if (typeof text !== 'string' || text.length === 0) {
return { success: false, error: 'Missing or empty "text" field' };
}
ctx.broadcast(SseEvent.ClipboardWrite, {
text,
sessionId: body.sessionId ?? null,
timestamp: Date.now(),
});
return { success: true };
});
}
+1
View File
@@ -15,4 +15,5 @@ export { registerRespawnRoutes } from './respawn-routes.js';
export { registerRalphRoutes } from './ralph-routes.js';
export { registerPlanRoutes } from './plan-routes.js';
export { registerOrchestratorRoutes } from './orchestrator-routes.js';
export { registerClipboardRoutes } from './clipboard-routes.js';
export { registerWsRoutes } from './ws-routes.js';
+2
View File
@@ -112,6 +112,7 @@ import {
registerRespawnRoutes,
registerRalphRoutes,
registerPlanRoutes,
registerClipboardRoutes,
registerOrchestratorRoutes,
registerWsRoutes,
} from './routes/index.js';
@@ -650,6 +651,7 @@ export class WebServer extends EventEmitter {
registerRespawnRoutes(this.app, ctx);
registerRalphRoutes(this.app, ctx);
registerPlanRoutes(this.app, ctx);
registerClipboardRoutes(this.app, ctx);
registerOrchestratorRoutes(this.app, ctx);
registerWsRoutes(this.app, ctx);
}
+8
View File
@@ -319,6 +319,11 @@ export const OrchestratorCompleted = 'orchestrator:completed' as const;
/** Orchestrator error. */
export const OrchestratorError = 'orchestrator:error' as const;
// ─── Clipboard ──────────────────────────────────────────────────────────────
/** Clipboard content pushed to browser. */
export const ClipboardWrite = 'clipboard:write' as const;
// ─── Cases ───────────────────────────────────────────────────────────────────
/** New case directory created. */
@@ -486,6 +491,9 @@ export const SseEvent = {
OrchestratorCompleted,
OrchestratorError,
// Clipboard
ClipboardWrite,
// Cases
CaseCreated,
CaseLinked,