mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Merge pull request #68 from aakhter/feat/clipboard-api
feat: add clipboard API for remote browser clipboard access
This commit is contained in:
@@ -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'],
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(); };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user