mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(terminal): an OSC 52 copy hands focus back to the terminal (#611 merge-time fix)
When navigator.clipboard is missing (plain HTTP, install.sh's LAN option) or refuses the write (an unfocused window), _copyText falls back to a hidden textarea whose select() takes focus, and removing it left focus on <body>: the next keystrokes after Claude Code's /copy went nowhere. The handler now restores the element focused before the copy, as Auto Copy does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f3d7a86c16
commit
f073742cf7
3 files changed
+34
-3
No files matched your search
@@ -29,11 +29,17 @@ const read = (rel: string) => readFileSync(resolve(import.meta.dirname, '..', re
|
||||
|
||||
function loadHarness(copyResult = true, focused = true) {
|
||||
const CodemanApp = function CodemanApp(this: unknown) {};
|
||||
const document: Record<string, unknown> = {
|
||||
getElementById: () => null,
|
||||
querySelector: () => null,
|
||||
hasFocus: () => focused,
|
||||
activeElement: null,
|
||||
};
|
||||
const context = vm.createContext({
|
||||
CodemanApp,
|
||||
window: {},
|
||||
globalThis: {},
|
||||
document: { getElementById: () => null, querySelector: () => null, hasFocus: () => focused },
|
||||
document,
|
||||
console,
|
||||
MobileDetection: { isTouchDevice: () => false, getDeviceType: () => 'desktop' },
|
||||
Object,
|
||||
@@ -64,7 +70,7 @@ function loadHarness(copyResult = true, focused = true) {
|
||||
const terminal = {
|
||||
parser: { registerOscHandler: (id: number, fn: (data: string) => boolean) => handlers.set(id, fn) },
|
||||
};
|
||||
return { app, terminal, handlers, copied, fallbacks, toasts };
|
||||
return { app, terminal, handlers, copied, fallbacks, toasts, document };
|
||||
}
|
||||
|
||||
const b64 = (text: string) => Buffer.from(text, 'utf8').toString('base64');
|
||||
@@ -120,6 +126,23 @@ describe('OSC 52 clipboard handler', () => {
|
||||
expect(h.toasts).toEqual([]);
|
||||
});
|
||||
|
||||
it('hands focus back when the copy fallback took it (plain HTTP, a refused write)', async () => {
|
||||
const h = loadHarness();
|
||||
let focusCalls = 0;
|
||||
const terminalTextarea = { isConnected: true, focus: () => focusCalls++ };
|
||||
h.document.activeElement = terminalTextarea;
|
||||
// _copyText's execCommand fallback selects a temporary textarea, then removes
|
||||
// it, which leaves focus on <body>.
|
||||
h.app._copyText = async () => {
|
||||
h.document.activeElement = { tagName: 'BODY' };
|
||||
return true;
|
||||
};
|
||||
h.app._installOsc52Clipboard(h.terminal);
|
||||
h.handlers.get(52)!(`c;${b64('copied')}`);
|
||||
await flush();
|
||||
expect(focusCalls).toBe(1);
|
||||
});
|
||||
|
||||
it('copies one Claude Code copy once, though it arrives three times', async () => {
|
||||
// What a single /copy in a Codeman tmux session actually delivers: Claude's
|
||||
// own OSC 52 plus tmux forwarding it twice (captured from a live session).
|
||||
|
||||
Reference in new issue
Block a user