mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29: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_TASK_FAILED, '_onOrchestratorTaskFailed'],
|
||||||
[SSE_EVENTS.ORCHESTRATOR_COMPLETED, '_onOrchestratorCompleted'],
|
[SSE_EVENTS.ORCHESTRATOR_COMPLETED, '_onOrchestratorCompleted'],
|
||||||
[SSE_EVENTS.ORCHESTRATOR_ERROR, '_onOrchestratorError'],
|
[SSE_EVENTS.ORCHESTRATOR_ERROR, '_onOrchestratorError'],
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
[SSE_EVENTS.CLIPBOARD_WRITE, '_onClipboardWrite'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,9 @@ const SSE_EVENTS = {
|
|||||||
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
|
ORCHESTRATOR_COMPLETED: 'orchestrator:completed',
|
||||||
ORCHESTRATOR_ERROR: 'orchestrator:error',
|
ORCHESTRATOR_ERROR: 'orchestrator:error',
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
CLIPBOARD_WRITE: 'clipboard:write',
|
||||||
|
|
||||||
// Cases
|
// Cases
|
||||||
CASE_CREATED: 'case:created',
|
CASE_CREATED: 'case:created',
|
||||||
CASE_LINKED: 'case:linked',
|
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 { registerRalphRoutes } from './ralph-routes.js';
|
||||||
export { registerPlanRoutes } from './plan-routes.js';
|
export { registerPlanRoutes } from './plan-routes.js';
|
||||||
export { registerOrchestratorRoutes } from './orchestrator-routes.js';
|
export { registerOrchestratorRoutes } from './orchestrator-routes.js';
|
||||||
|
export { registerClipboardRoutes } from './clipboard-routes.js';
|
||||||
export { registerWsRoutes } from './ws-routes.js';
|
export { registerWsRoutes } from './ws-routes.js';
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ import {
|
|||||||
registerRespawnRoutes,
|
registerRespawnRoutes,
|
||||||
registerRalphRoutes,
|
registerRalphRoutes,
|
||||||
registerPlanRoutes,
|
registerPlanRoutes,
|
||||||
|
registerClipboardRoutes,
|
||||||
registerOrchestratorRoutes,
|
registerOrchestratorRoutes,
|
||||||
registerWsRoutes,
|
registerWsRoutes,
|
||||||
} from './routes/index.js';
|
} from './routes/index.js';
|
||||||
@@ -650,6 +651,7 @@ export class WebServer extends EventEmitter {
|
|||||||
registerRespawnRoutes(this.app, ctx);
|
registerRespawnRoutes(this.app, ctx);
|
||||||
registerRalphRoutes(this.app, ctx);
|
registerRalphRoutes(this.app, ctx);
|
||||||
registerPlanRoutes(this.app, ctx);
|
registerPlanRoutes(this.app, ctx);
|
||||||
|
registerClipboardRoutes(this.app, ctx);
|
||||||
registerOrchestratorRoutes(this.app, ctx);
|
registerOrchestratorRoutes(this.app, ctx);
|
||||||
registerWsRoutes(this.app, ctx);
|
registerWsRoutes(this.app, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,6 +319,11 @@ export const OrchestratorCompleted = 'orchestrator:completed' as const;
|
|||||||
/** Orchestrator error. */
|
/** Orchestrator error. */
|
||||||
export const OrchestratorError = 'orchestrator:error' as const;
|
export const OrchestratorError = 'orchestrator:error' as const;
|
||||||
|
|
||||||
|
// ─── Clipboard ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Clipboard content pushed to browser. */
|
||||||
|
export const ClipboardWrite = 'clipboard:write' as const;
|
||||||
|
|
||||||
// ─── Cases ───────────────────────────────────────────────────────────────────
|
// ─── Cases ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** New case directory created. */
|
/** New case directory created. */
|
||||||
@@ -486,6 +491,9 @@ export const SseEvent = {
|
|||||||
OrchestratorCompleted,
|
OrchestratorCompleted,
|
||||||
OrchestratorError,
|
OrchestratorError,
|
||||||
|
|
||||||
|
// Clipboard
|
||||||
|
ClipboardWrite,
|
||||||
|
|
||||||
// Cases
|
// Cases
|
||||||
CaseCreated,
|
CaseCreated,
|
||||||
CaseLinked,
|
CaseLinked,
|
||||||
|
|||||||
Reference in New Issue
Block a user