claude-remote 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server.js +469 -241
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -184,22 +184,23 @@ let pendingInitialClearTranscript = null; // { sessionId }
|
|
|
184
184
|
let tailRemainder = Buffer.alloc(0);
|
|
185
185
|
let tailCatchingUp = false; // true while reading historical transcript content
|
|
186
186
|
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
187
|
-
const LEGACY_REPLAY_DELAY_MS = 1500;
|
|
187
|
+
const LEGACY_REPLAY_DELAY_MS = 1500;
|
|
188
188
|
const IMAGE_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
189
189
|
const LINUX_CLIPBOARD_READY_GRACE_MS = 400;
|
|
190
|
-
const
|
|
191
|
-
|
|
190
|
+
const LINUX_AT_PROMPT_SUBMIT_DELAY_MS = 450;
|
|
191
|
+
const LINUX_AT_IMAGE_CLEANUP_DELAY_MS = 10 * 60 * 1000;
|
|
192
|
+
let turnStateVersion = 0;
|
|
192
193
|
let turnState = {
|
|
193
194
|
phase: 'idle',
|
|
194
195
|
sessionId: null,
|
|
195
196
|
version: 0,
|
|
196
197
|
updatedAt: Date.now(),
|
|
197
198
|
};
|
|
198
|
-
let ttyInputForwarderAttached = false;
|
|
199
|
-
let ttyInputHandler = null;
|
|
200
|
-
let ttyResizeHandler = null;
|
|
201
|
-
let activeLinuxClipboardProc = null;
|
|
202
|
-
let linuxImagePasteInFlight = false;
|
|
199
|
+
let ttyInputForwarderAttached = false;
|
|
200
|
+
let ttyInputHandler = null;
|
|
201
|
+
let ttyResizeHandler = null;
|
|
202
|
+
let activeLinuxClipboardProc = null;
|
|
203
|
+
let linuxImagePasteInFlight = false;
|
|
203
204
|
|
|
204
205
|
// --- Permission approval state ---
|
|
205
206
|
let approvalSeq = 0;
|
|
@@ -222,10 +223,10 @@ function formatTtyInputChunk(chunk) {
|
|
|
222
223
|
return `len=${buf.length} hex=${buf.toString('hex')} base64=${buf.toString('base64')} utf8=${JSON.stringify(buf.toString('utf8'))}`;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
function clearActiveLinuxClipboardProc(reason = '') {
|
|
226
|
-
if (!activeLinuxClipboardProc) return;
|
|
227
|
-
const { child, tool } = activeLinuxClipboardProc;
|
|
228
|
-
activeLinuxClipboardProc = null;
|
|
226
|
+
function clearActiveLinuxClipboardProc(reason = '') {
|
|
227
|
+
if (!activeLinuxClipboardProc) return;
|
|
228
|
+
const { child, tool } = activeLinuxClipboardProc;
|
|
229
|
+
activeLinuxClipboardProc = null;
|
|
229
230
|
try {
|
|
230
231
|
child.kill('SIGTERM');
|
|
231
232
|
log(`Linux clipboard process terminated (${tool}) reason=${reason || 'cleanup'}`);
|
|
@@ -683,145 +684,362 @@ function cleanupClientUploads(ws) {
|
|
|
683
684
|
}
|
|
684
685
|
}
|
|
685
686
|
|
|
686
|
-
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
687
|
-
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
688
|
-
const tmpDir = isLinux
|
|
689
|
-
?
|
|
690
|
-
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
691
|
-
const type = String(mediaType || 'image/png').toLowerCase();
|
|
692
|
-
const ext = type.includes('jpeg') || type.includes('jpg') ? '.jpg' : '.png';
|
|
693
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
694
|
-
const tmpFile = path.join(tmpDir, `bridge_upload_${uploadId}_${Date.now()}${ext}`);
|
|
695
|
-
fs.writeFileSync(tmpFile, buffer);
|
|
696
|
-
return tmpFile;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function isLinuxClipboardToolInstalled(tool) {
|
|
700
|
-
try {
|
|
701
|
-
execSync(`command -v ${tool} >/dev/null 2>&1`, {
|
|
702
|
-
stdio: 'ignore',
|
|
703
|
-
shell: '/bin/sh',
|
|
704
|
-
timeout: 2000,
|
|
705
|
-
});
|
|
706
|
-
return true;
|
|
707
|
-
} catch {
|
|
708
|
-
return false;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function setLinuxImagePasteInFlight(active, reason = '') {
|
|
713
|
-
linuxImagePasteInFlight = !!active;
|
|
714
|
-
if (reason) log(`Linux image paste lock=${linuxImagePasteInFlight ? 'on' : 'off'} reason=${reason}`);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function getLinuxClipboardToolCandidates() {
|
|
718
|
-
if (process.platform === 'win32' || process.platform === 'darwin') return [];
|
|
719
|
-
const preferred = [];
|
|
720
|
-
if (process.env.WAYLAND_DISPLAY) preferred.push('wl-copy');
|
|
721
|
-
if (process.env.DISPLAY) preferred.push('xclip');
|
|
722
|
-
return preferred;
|
|
687
|
+
function createTempImageFile(buffer, mediaType, uploadId) {
|
|
688
|
+
const isLinux = process.platform !== 'win32' && process.platform !== 'darwin';
|
|
689
|
+
const tmpDir = isLinux
|
|
690
|
+
? path.join(CWD, 'tmp')
|
|
691
|
+
: (process.env.CLAUDE_CODE_TMPDIR || os.tmpdir());
|
|
692
|
+
const type = String(mediaType || 'image/png').toLowerCase();
|
|
693
|
+
const ext = type.includes('jpeg') || type.includes('jpg') ? '.jpg' : '.png';
|
|
694
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
695
|
+
const tmpFile = path.join(tmpDir, `bridge_upload_${uploadId}_${Date.now()}${ext}`);
|
|
696
|
+
fs.writeFileSync(tmpFile, buffer);
|
|
697
|
+
return tmpFile;
|
|
723
698
|
}
|
|
724
699
|
|
|
725
|
-
function
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
throw new Error('Linux image paste requires wl-copy or xclip on the server. Install a matching clipboard tool and try again.');
|
|
700
|
+
function toClaudeAtPath(filePath) {
|
|
701
|
+
const normalized = path.normalize(String(filePath || ''));
|
|
702
|
+
const rel = path.relative(CWD, normalized);
|
|
703
|
+
const inProject = rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
704
|
+
const target = inProject ? rel : normalized;
|
|
705
|
+
return target.split(path.sep).join('/');
|
|
733
706
|
}
|
|
734
707
|
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
: ['--type', type];
|
|
740
|
-
const child = spawn(tool, args, {
|
|
741
|
-
detached: true,
|
|
742
|
-
stdio: ['pipe', 'ignore', 'pipe'],
|
|
743
|
-
});
|
|
744
|
-
let settled = false;
|
|
745
|
-
let stderr = '';
|
|
746
|
-
let readyTimer = null;
|
|
747
|
-
|
|
748
|
-
const settleFailure = (message) => {
|
|
749
|
-
if (settled) return;
|
|
750
|
-
settled = true;
|
|
751
|
-
if (readyTimer) clearTimeout(readyTimer);
|
|
752
|
-
if (child.exitCode == null && child.signalCode == null) {
|
|
753
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
754
|
-
}
|
|
755
|
-
reject(new Error(message));
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
const settleSuccess = (trackProcess = true) => {
|
|
759
|
-
if (settled) return;
|
|
760
|
-
settled = true;
|
|
761
|
-
if (readyTimer) clearTimeout(readyTimer);
|
|
762
|
-
if (trackProcess && child.exitCode == null && child.signalCode == null) {
|
|
763
|
-
activeLinuxClipboardProc = { child, tool };
|
|
764
|
-
child.unref();
|
|
765
|
-
}
|
|
766
|
-
resolve(tool);
|
|
767
|
-
};
|
|
768
|
-
|
|
769
|
-
child.on('error', (err) => {
|
|
770
|
-
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
771
|
-
settleFailure(`Linux clipboard tool ${tool} failed: ${err.message}`);
|
|
772
|
-
});
|
|
773
|
-
child.stderr.on('data', (chunk) => {
|
|
774
|
-
stderr += chunk.toString('utf8');
|
|
775
|
-
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
776
|
-
});
|
|
777
|
-
child.on('exit', (code, signal) => {
|
|
778
|
-
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
779
|
-
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
780
|
-
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
781
|
-
if (!settled) {
|
|
782
|
-
if (tool === 'xclip' && code === 0 && !signal && !stderr.trim()) {
|
|
783
|
-
log('Linux clipboard xclip exited cleanly without stderr; treating clipboard arm as successful');
|
|
784
|
-
settleSuccess(false);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const detail = stderr.trim() || `exit code ${code ?? 'null'} signal ${signal ?? 'null'}`;
|
|
788
|
-
settleFailure(`Linux clipboard tool ${tool} exited before paste: ${detail}`);
|
|
789
|
-
}
|
|
790
|
-
});
|
|
791
|
-
child.stdin.on('error', (err) => {
|
|
792
|
-
if (err.code === 'EPIPE') {
|
|
793
|
-
settleFailure(`Linux clipboard tool ${tool} closed its input early`);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
797
|
-
settleFailure(`Linux clipboard tool ${tool} stdin failed: ${err.message}`);
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
child.stdin.end(imageBuffer);
|
|
801
|
-
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
802
|
-
readyTimer = setTimeout(() => settleSuccess(), LINUX_CLIPBOARD_READY_GRACE_MS);
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
async function startLinuxClipboardImage(tmpFile, mediaType) {
|
|
807
|
-
const type = String(mediaType || 'image/png').toLowerCase();
|
|
808
|
-
const imageBuffer = fs.readFileSync(tmpFile);
|
|
809
|
-
const availableTools = assertLinuxClipboardAvailable();
|
|
810
|
-
clearActiveLinuxClipboardProc('replace');
|
|
811
|
-
|
|
812
|
-
let lastErr = null;
|
|
813
|
-
for (const tool of availableTools) {
|
|
814
|
-
try {
|
|
815
|
-
return await spawnLinuxClipboardTool(tool, imageBuffer, type);
|
|
816
|
-
} catch (err) {
|
|
817
|
-
lastErr = err;
|
|
818
|
-
log(`Linux clipboard arm failed (${tool}): ${err.message}`);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
throw lastErr || new Error('Linux clipboard could not be initialized');
|
|
708
|
+
function buildLinuxImagePrompt(text, tmpFile) {
|
|
709
|
+
const trimmedText = String(text || '').trim();
|
|
710
|
+
const atPath = `@${toClaudeAtPath(tmpFile)}`;
|
|
711
|
+
return trimmedText ? `${trimmedText} ${atPath}` : atPath;
|
|
823
712
|
}
|
|
824
713
|
|
|
714
|
+
function isLinuxClipboardToolInstalled(tool) {
|
|
715
|
+
try {
|
|
716
|
+
execSync(`command -v ${tool} >/dev/null 2>&1`, {
|
|
717
|
+
stdio: 'ignore',
|
|
718
|
+
shell: '/bin/sh',
|
|
719
|
+
timeout: 2000,
|
|
720
|
+
});
|
|
721
|
+
return true;
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function setLinuxImagePasteInFlight(active, reason = '') {
|
|
728
|
+
linuxImagePasteInFlight = !!active;
|
|
729
|
+
if (reason) log(`Linux image paste lock=${linuxImagePasteInFlight ? 'on' : 'off'} reason=${reason}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function normalizeLinuxEnvVar(value) {
|
|
733
|
+
const text = String(value || '').trim();
|
|
734
|
+
return text || null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function parseLinuxProcStatusUid(statusText) {
|
|
738
|
+
const match = String(statusText || '').match(/^Uid:\s+(\d+)/m);
|
|
739
|
+
return match ? Number(match[1]) : null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function readLinuxProcGuiEnv(pid) {
|
|
743
|
+
try {
|
|
744
|
+
const statusPath = `/proc/${pid}/status`;
|
|
745
|
+
const environPath = `/proc/${pid}/environ`;
|
|
746
|
+
const statusText = fs.readFileSync(statusPath, 'utf8');
|
|
747
|
+
const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
748
|
+
if (currentUid != null) {
|
|
749
|
+
const procUid = parseLinuxProcStatusUid(statusText);
|
|
750
|
+
if (procUid == null || procUid !== currentUid) return null;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const envRaw = fs.readFileSync(environPath, 'utf8');
|
|
754
|
+
if (!envRaw) return null;
|
|
755
|
+
let waylandDisplay = null;
|
|
756
|
+
let display = null;
|
|
757
|
+
let runtimeDir = null;
|
|
758
|
+
let xAuthority = null;
|
|
759
|
+
|
|
760
|
+
for (const entry of envRaw.split('\0')) {
|
|
761
|
+
if (!entry) continue;
|
|
762
|
+
if (entry.startsWith('WAYLAND_DISPLAY=')) waylandDisplay = normalizeLinuxEnvVar(entry.slice('WAYLAND_DISPLAY='.length));
|
|
763
|
+
else if (entry.startsWith('DISPLAY=')) display = normalizeLinuxEnvVar(entry.slice('DISPLAY='.length));
|
|
764
|
+
else if (entry.startsWith('XDG_RUNTIME_DIR=')) runtimeDir = normalizeLinuxEnvVar(entry.slice('XDG_RUNTIME_DIR='.length));
|
|
765
|
+
else if (entry.startsWith('XAUTHORITY=')) xAuthority = normalizeLinuxEnvVar(entry.slice('XAUTHORITY='.length));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (!waylandDisplay && !display) return null;
|
|
769
|
+
return { waylandDisplay, display, runtimeDir, xAuthority };
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function discoverLinuxGuiEnvFromProc() {
|
|
776
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return null;
|
|
777
|
+
let entries = [];
|
|
778
|
+
try {
|
|
779
|
+
entries = fs.readdirSync('/proc', { withFileTypes: true });
|
|
780
|
+
} catch {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for (const entry of entries) {
|
|
785
|
+
if (!entry.isDirectory()) continue;
|
|
786
|
+
if (!/^\d+$/.test(entry.name)) continue;
|
|
787
|
+
if (Number(entry.name) === process.pid) continue;
|
|
788
|
+
const discovered = readLinuxProcGuiEnv(entry.name);
|
|
789
|
+
if (discovered) return discovered;
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function discoverLinuxGuiEnvFromSocket() {
|
|
795
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return null;
|
|
796
|
+
const discovered = {
|
|
797
|
+
waylandDisplay: null,
|
|
798
|
+
display: null,
|
|
799
|
+
runtimeDir: null,
|
|
800
|
+
xAuthority: null,
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const currentUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
804
|
+
const runtimeDir = currentUid != null ? `/run/user/${currentUid}` : null;
|
|
805
|
+
if (runtimeDir && fs.existsSync(runtimeDir)) {
|
|
806
|
+
discovered.runtimeDir = runtimeDir;
|
|
807
|
+
try {
|
|
808
|
+
const entries = fs.readdirSync(runtimeDir);
|
|
809
|
+
const waylandSockets = entries.filter(name => /^wayland-\d+$/.test(name)).sort();
|
|
810
|
+
if (waylandSockets.length > 0) discovered.waylandDisplay = waylandSockets[0];
|
|
811
|
+
} catch {}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
const xEntries = fs.readdirSync('/tmp/.X11-unix');
|
|
816
|
+
const displaySockets = xEntries
|
|
817
|
+
.map(name => {
|
|
818
|
+
const match = /^X(\d+)$/.exec(name);
|
|
819
|
+
return match ? Number(match[1]) : null;
|
|
820
|
+
})
|
|
821
|
+
.filter(num => Number.isInteger(num))
|
|
822
|
+
.sort((a, b) => a - b);
|
|
823
|
+
if (displaySockets.length > 0) discovered.display = `:${displaySockets[0]}`;
|
|
824
|
+
} catch {}
|
|
825
|
+
|
|
826
|
+
if (!discovered.waylandDisplay && !discovered.display) return null;
|
|
827
|
+
return discovered;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function getLinuxClipboardEnv() {
|
|
831
|
+
if (process.platform === 'win32' || process.platform === 'darwin') {
|
|
832
|
+
return { env: process.env, source: 'not_linux' };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const overlay = {
|
|
836
|
+
WAYLAND_DISPLAY: normalizeLinuxEnvVar(process.env.CLAUDE_REMOTE_WAYLAND_DISPLAY) || normalizeLinuxEnvVar(process.env.WAYLAND_DISPLAY),
|
|
837
|
+
DISPLAY: normalizeLinuxEnvVar(process.env.CLAUDE_REMOTE_DISPLAY) || normalizeLinuxEnvVar(process.env.DISPLAY),
|
|
838
|
+
XDG_RUNTIME_DIR: normalizeLinuxEnvVar(process.env.CLAUDE_REMOTE_XDG_RUNTIME_DIR) || normalizeLinuxEnvVar(process.env.XDG_RUNTIME_DIR),
|
|
839
|
+
XAUTHORITY: normalizeLinuxEnvVar(process.env.CLAUDE_REMOTE_XAUTHORITY) || normalizeLinuxEnvVar(process.env.XAUTHORITY),
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
let source = 'process_env';
|
|
843
|
+
const needsSocketDiscovery =
|
|
844
|
+
(!overlay.WAYLAND_DISPLAY && !overlay.DISPLAY) ||
|
|
845
|
+
(!!overlay.WAYLAND_DISPLAY && !overlay.XDG_RUNTIME_DIR);
|
|
846
|
+
if (needsSocketDiscovery) {
|
|
847
|
+
const before = {
|
|
848
|
+
waylandDisplay: overlay.WAYLAND_DISPLAY,
|
|
849
|
+
display: overlay.DISPLAY,
|
|
850
|
+
runtimeDir: overlay.XDG_RUNTIME_DIR,
|
|
851
|
+
xAuthority: overlay.XAUTHORITY,
|
|
852
|
+
};
|
|
853
|
+
const fromSocket = discoverLinuxGuiEnvFromSocket();
|
|
854
|
+
if (fromSocket) {
|
|
855
|
+
if (!overlay.WAYLAND_DISPLAY && fromSocket.waylandDisplay) overlay.WAYLAND_DISPLAY = fromSocket.waylandDisplay;
|
|
856
|
+
if (!overlay.DISPLAY && fromSocket.display) overlay.DISPLAY = fromSocket.display;
|
|
857
|
+
if (!overlay.XDG_RUNTIME_DIR && fromSocket.runtimeDir) overlay.XDG_RUNTIME_DIR = fromSocket.runtimeDir;
|
|
858
|
+
if (!overlay.XAUTHORITY && fromSocket.xAuthority) overlay.XAUTHORITY = fromSocket.xAuthority;
|
|
859
|
+
const changed =
|
|
860
|
+
before.waylandDisplay !== overlay.WAYLAND_DISPLAY ||
|
|
861
|
+
before.display !== overlay.DISPLAY ||
|
|
862
|
+
before.runtimeDir !== overlay.XDG_RUNTIME_DIR ||
|
|
863
|
+
before.xAuthority !== overlay.XAUTHORITY;
|
|
864
|
+
if (changed) source = 'socket_discovery';
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const needsProcDiscovery =
|
|
869
|
+
(!overlay.WAYLAND_DISPLAY && !overlay.DISPLAY) ||
|
|
870
|
+
(!!overlay.DISPLAY && !overlay.XAUTHORITY) ||
|
|
871
|
+
(!!overlay.WAYLAND_DISPLAY && !overlay.XDG_RUNTIME_DIR);
|
|
872
|
+
if (needsProcDiscovery) {
|
|
873
|
+
const before = {
|
|
874
|
+
waylandDisplay: overlay.WAYLAND_DISPLAY,
|
|
875
|
+
display: overlay.DISPLAY,
|
|
876
|
+
runtimeDir: overlay.XDG_RUNTIME_DIR,
|
|
877
|
+
xAuthority: overlay.XAUTHORITY,
|
|
878
|
+
};
|
|
879
|
+
const fromProc = discoverLinuxGuiEnvFromProc();
|
|
880
|
+
if (fromProc) {
|
|
881
|
+
if (!overlay.WAYLAND_DISPLAY && fromProc.waylandDisplay) overlay.WAYLAND_DISPLAY = fromProc.waylandDisplay;
|
|
882
|
+
if (!overlay.DISPLAY && fromProc.display) overlay.DISPLAY = fromProc.display;
|
|
883
|
+
if (!overlay.XDG_RUNTIME_DIR && fromProc.runtimeDir) overlay.XDG_RUNTIME_DIR = fromProc.runtimeDir;
|
|
884
|
+
if (!overlay.XAUTHORITY && fromProc.xAuthority) overlay.XAUTHORITY = fromProc.xAuthority;
|
|
885
|
+
const changed =
|
|
886
|
+
before.waylandDisplay !== overlay.WAYLAND_DISPLAY ||
|
|
887
|
+
before.display !== overlay.DISPLAY ||
|
|
888
|
+
before.runtimeDir !== overlay.XDG_RUNTIME_DIR ||
|
|
889
|
+
before.xAuthority !== overlay.XAUTHORITY;
|
|
890
|
+
if (changed) {
|
|
891
|
+
source = source === 'socket_discovery' ? 'socket+proc_discovery' : 'proc_discovery';
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const env = { ...process.env };
|
|
897
|
+
if (overlay.WAYLAND_DISPLAY) env.WAYLAND_DISPLAY = overlay.WAYLAND_DISPLAY;
|
|
898
|
+
if (overlay.DISPLAY) env.DISPLAY = overlay.DISPLAY;
|
|
899
|
+
if (overlay.XDG_RUNTIME_DIR) env.XDG_RUNTIME_DIR = overlay.XDG_RUNTIME_DIR;
|
|
900
|
+
if (overlay.XAUTHORITY) env.XAUTHORITY = overlay.XAUTHORITY;
|
|
901
|
+
|
|
902
|
+
return {
|
|
903
|
+
env,
|
|
904
|
+
source,
|
|
905
|
+
waylandDisplay: overlay.WAYLAND_DISPLAY || null,
|
|
906
|
+
display: overlay.DISPLAY || null,
|
|
907
|
+
runtimeDir: overlay.XDG_RUNTIME_DIR || null,
|
|
908
|
+
xAuthority: overlay.XAUTHORITY || null,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function getLinuxClipboardToolCandidates(clipboardEnv = process.env) {
|
|
913
|
+
if (process.platform === 'win32' || process.platform === 'darwin') return [];
|
|
914
|
+
const preferred = [];
|
|
915
|
+
if (clipboardEnv.WAYLAND_DISPLAY) preferred.push('wl-copy');
|
|
916
|
+
if (clipboardEnv.DISPLAY) preferred.push('xclip');
|
|
917
|
+
return preferred;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function assertLinuxClipboardAvailable() {
|
|
921
|
+
const gui = getLinuxClipboardEnv();
|
|
922
|
+
const candidates = getLinuxClipboardToolCandidates(gui.env);
|
|
923
|
+
const available = candidates.filter(isLinuxClipboardToolInstalled);
|
|
924
|
+
if (available.length > 0) {
|
|
925
|
+
return {
|
|
926
|
+
tools: available,
|
|
927
|
+
env: gui.env,
|
|
928
|
+
source: gui.source,
|
|
929
|
+
waylandDisplay: gui.waylandDisplay,
|
|
930
|
+
display: gui.display,
|
|
931
|
+
runtimeDir: gui.runtimeDir,
|
|
932
|
+
xAuthority: gui.xAuthority,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (!gui.waylandDisplay && !gui.display) {
|
|
936
|
+
throw new Error('Linux image paste requires a graphical session. Could not detect WAYLAND_DISPLAY or DISPLAY (common in pm2/systemd). Set CLAUDE_REMOTE_DISPLAY or CLAUDE_REMOTE_WAYLAND_DISPLAY and retry.');
|
|
937
|
+
}
|
|
938
|
+
throw new Error('Linux image paste requires wl-copy or xclip on the server. Install a matching clipboard tool and try again.');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function formatLinuxClipboardEnvLog(info) {
|
|
942
|
+
if (!info) return '';
|
|
943
|
+
const parts = [];
|
|
944
|
+
if (info.waylandDisplay) parts.push(`WAYLAND_DISPLAY=${info.waylandDisplay}`);
|
|
945
|
+
if (info.display) parts.push(`DISPLAY=${info.display}`);
|
|
946
|
+
if (info.runtimeDir) parts.push(`XDG_RUNTIME_DIR=${info.runtimeDir}`);
|
|
947
|
+
if (info.xAuthority) parts.push(`XAUTHORITY=${info.xAuthority}`);
|
|
948
|
+
return parts.length ? ` env[${parts.join(', ')}]` : '';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function spawnLinuxClipboardTool(tool, imageBuffer, type, clipboardEnv) {
|
|
952
|
+
return new Promise((resolve, reject) => {
|
|
953
|
+
const args = tool === 'xclip'
|
|
954
|
+
? ['-quiet', '-selection', 'clipboard', '-t', type, '-i']
|
|
955
|
+
: ['--type', type];
|
|
956
|
+
const child = spawn(tool, args, {
|
|
957
|
+
detached: true,
|
|
958
|
+
stdio: ['pipe', 'ignore', 'pipe'],
|
|
959
|
+
env: clipboardEnv || process.env,
|
|
960
|
+
});
|
|
961
|
+
let settled = false;
|
|
962
|
+
let stderr = '';
|
|
963
|
+
let readyTimer = null;
|
|
964
|
+
|
|
965
|
+
const settleFailure = (message) => {
|
|
966
|
+
if (settled) return;
|
|
967
|
+
settled = true;
|
|
968
|
+
if (readyTimer) clearTimeout(readyTimer);
|
|
969
|
+
if (child.exitCode == null && child.signalCode == null) {
|
|
970
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
971
|
+
}
|
|
972
|
+
reject(new Error(message));
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const settleSuccess = (trackProcess = true) => {
|
|
976
|
+
if (settled) return;
|
|
977
|
+
settled = true;
|
|
978
|
+
if (readyTimer) clearTimeout(readyTimer);
|
|
979
|
+
if (trackProcess && child.exitCode == null && child.signalCode == null) {
|
|
980
|
+
activeLinuxClipboardProc = { child, tool };
|
|
981
|
+
child.unref();
|
|
982
|
+
}
|
|
983
|
+
resolve(tool);
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
child.on('error', (err) => {
|
|
987
|
+
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
988
|
+
settleFailure(`Linux clipboard tool ${tool} failed: ${err.message}`);
|
|
989
|
+
});
|
|
990
|
+
child.stderr.on('data', (chunk) => {
|
|
991
|
+
stderr += chunk.toString('utf8');
|
|
992
|
+
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
993
|
+
});
|
|
994
|
+
child.on('exit', (code, signal) => {
|
|
995
|
+
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
996
|
+
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
997
|
+
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
998
|
+
if (!settled) {
|
|
999
|
+
if (tool === 'xclip' && code === 0 && !signal && !stderr.trim()) {
|
|
1000
|
+
log('Linux clipboard xclip exited cleanly without stderr; treating clipboard arm as successful');
|
|
1001
|
+
settleSuccess(false);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const detail = stderr.trim() || `exit code ${code ?? 'null'} signal ${signal ?? 'null'}`;
|
|
1005
|
+
settleFailure(`Linux clipboard tool ${tool} exited before paste: ${detail}`);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
child.stdin.on('error', (err) => {
|
|
1009
|
+
if (err.code === 'EPIPE') {
|
|
1010
|
+
settleFailure(`Linux clipboard tool ${tool} closed its input early`);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
1014
|
+
settleFailure(`Linux clipboard tool ${tool} stdin failed: ${err.message}`);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
child.stdin.end(imageBuffer);
|
|
1018
|
+
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
1019
|
+
readyTimer = setTimeout(() => settleSuccess(), LINUX_CLIPBOARD_READY_GRACE_MS);
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
async function startLinuxClipboardImage(tmpFile, mediaType, clipboardInfo = null) {
|
|
1024
|
+
const type = String(mediaType || 'image/png').toLowerCase();
|
|
1025
|
+
const imageBuffer = fs.readFileSync(tmpFile);
|
|
1026
|
+
const resolved = clipboardInfo || assertLinuxClipboardAvailable();
|
|
1027
|
+
const availableTools = resolved.tools;
|
|
1028
|
+
clearActiveLinuxClipboardProc('replace');
|
|
1029
|
+
|
|
1030
|
+
let lastErr = null;
|
|
1031
|
+
for (const tool of availableTools) {
|
|
1032
|
+
try {
|
|
1033
|
+
return await spawnLinuxClipboardTool(tool, imageBuffer, type, resolved.env);
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
lastErr = err;
|
|
1036
|
+
log(`Linux clipboard arm failed (${tool}): ${err.message}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
throw lastErr || new Error('Linux clipboard could not be initialized');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
825
1043
|
setInterval(() => {
|
|
826
1044
|
const now = Date.now();
|
|
827
1045
|
for (const [uploadId, upload] of pendingImageUploads) {
|
|
@@ -1064,26 +1282,17 @@ wss.on('connection', (ws, req) => {
|
|
|
1064
1282
|
}
|
|
1065
1283
|
break;
|
|
1066
1284
|
}
|
|
1067
|
-
case 'image_upload_init': {
|
|
1068
|
-
const uploadId = String(msg.uploadId || '');
|
|
1069
|
-
if (!uploadId) {
|
|
1070
|
-
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1073
|
-
cleanupImageUpload(uploadId);
|
|
1074
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1075
|
-
try {
|
|
1076
|
-
const tools = assertLinuxClipboardAvailable();
|
|
1077
|
-
log(`Linux clipboard preflight OK: ${tools.join(', ')}`);
|
|
1078
|
-
} catch (err) {
|
|
1079
|
-
sendUploadStatus(ws, uploadId, 'error', { message: err.message });
|
|
1080
|
-
break;
|
|
1081
|
-
}
|
|
1285
|
+
case 'image_upload_init': {
|
|
1286
|
+
const uploadId = String(msg.uploadId || '');
|
|
1287
|
+
if (!uploadId) {
|
|
1288
|
+
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1289
|
+
break;
|
|
1082
1290
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1291
|
+
cleanupImageUpload(uploadId);
|
|
1292
|
+
pendingImageUploads.set(uploadId, {
|
|
1293
|
+
id: uploadId,
|
|
1294
|
+
owner: ws,
|
|
1295
|
+
mediaType: msg.mediaType || 'image/png',
|
|
1087
1296
|
name: msg.name || 'image',
|
|
1088
1297
|
totalBytes: Number.isFinite(msg.totalBytes) ? msg.totalBytes : 0,
|
|
1089
1298
|
totalChunks: Number.isFinite(msg.totalChunks) ? msg.totalChunks : 0,
|
|
@@ -1181,13 +1390,13 @@ wss.on('connection', (ws, req) => {
|
|
|
1181
1390
|
sendUploadStatus(ws, uploadId, 'error', { message: 'Upload not ready' });
|
|
1182
1391
|
break;
|
|
1183
1392
|
}
|
|
1184
|
-
try {
|
|
1185
|
-
await handlePreparedImageUpload({
|
|
1186
|
-
tmpFile: upload.tmpFile,
|
|
1187
|
-
mediaType: upload.mediaType,
|
|
1188
|
-
text: msg.text || '',
|
|
1189
|
-
logLabel: upload.name || uploadId,
|
|
1190
|
-
onCleanup: () => cleanupImageUpload(uploadId),
|
|
1393
|
+
try {
|
|
1394
|
+
await handlePreparedImageUpload({
|
|
1395
|
+
tmpFile: upload.tmpFile,
|
|
1396
|
+
mediaType: upload.mediaType,
|
|
1397
|
+
text: msg.text || '',
|
|
1398
|
+
logLabel: upload.name || uploadId,
|
|
1399
|
+
onCleanup: () => cleanupImageUpload(uploadId),
|
|
1191
1400
|
});
|
|
1192
1401
|
upload.submitted = true;
|
|
1193
1402
|
upload.updatedAt = Date.now();
|
|
@@ -1396,11 +1605,20 @@ function extractSlashCommand(content) {
|
|
|
1396
1605
|
return inlineMatch ? inlineMatch[1].trim().toLowerCase() : '';
|
|
1397
1606
|
}
|
|
1398
1607
|
|
|
1608
|
+
function isUserInterruptEvent(content) {
|
|
1609
|
+
const text = flattenUserContent(content)
|
|
1610
|
+
.replace(/\x1B\[[0-9;]*m/g, '')
|
|
1611
|
+
.trim();
|
|
1612
|
+
if (!text) return false;
|
|
1613
|
+
return /(?:^|\n)\[Request interrupted by user(?: for tool use)?\](?:\r?\n|$)/i.test(text);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1399
1616
|
function isNonAiUserEvent(event, content) {
|
|
1400
1617
|
if (!event || typeof event !== 'object') return false;
|
|
1401
1618
|
if (event.isMeta === true) return true;
|
|
1402
1619
|
if (event.isCompactSummary === true) return true;
|
|
1403
1620
|
if (event.isVisibleInTranscriptOnly === true) return true;
|
|
1621
|
+
if (isUserInterruptEvent(content)) return true;
|
|
1404
1622
|
|
|
1405
1623
|
const text = flattenUserContent(content).trim();
|
|
1406
1624
|
if (!text) return false;
|
|
@@ -1554,12 +1772,16 @@ function startTailing() {
|
|
|
1554
1772
|
if (event.type === 'user' || (event.message && event.message.role === 'user')) {
|
|
1555
1773
|
const content = event.message && event.message.content;
|
|
1556
1774
|
const slashCommand = extractSlashCommand(content);
|
|
1775
|
+
const isInterruptedUserEvent = isUserInterruptEvent(content);
|
|
1557
1776
|
const isPassiveUserEvent = isNonAiUserEvent(event, content);
|
|
1558
1777
|
const ignoreInitialClear = (
|
|
1559
1778
|
slashCommand === '/clear' &&
|
|
1560
1779
|
pendingInitialClearTranscript &&
|
|
1561
1780
|
pendingInitialClearTranscript.sessionId === currentSessionId
|
|
1562
1781
|
);
|
|
1782
|
+
if (!tailCatchingUp && isInterruptedUserEvent) {
|
|
1783
|
+
setTurnState('idle', { sessionId: currentSessionId, reason: 'transcript_user_interrupt' });
|
|
1784
|
+
}
|
|
1563
1785
|
// Only live, AI-producing user messages can move the turn state
|
|
1564
1786
|
// into running. Historical replay and slash commands are ignored.
|
|
1565
1787
|
if (!tailCatchingUp && !slashCommand && !isPassiveUserEvent) {
|
|
@@ -1815,81 +2037,91 @@ function restartClaude(newCwd) {
|
|
|
1815
2037
|
// ============================================================
|
|
1816
2038
|
// 5. Image Upload → Clipboard Injection
|
|
1817
2039
|
// ============================================================
|
|
1818
|
-
async function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', onCleanup = null }) {
|
|
1819
|
-
if (!claudeProc) throw new Error('Claude not running');
|
|
1820
|
-
if (!tmpFile || !fs.existsSync(tmpFile)) throw new Error('Prepared image file missing');
|
|
1821
|
-
|
|
2040
|
+
async function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', onCleanup = null }) {
|
|
2041
|
+
if (!claudeProc) throw new Error('Claude not running');
|
|
2042
|
+
if (!tmpFile || !fs.existsSync(tmpFile)) throw new Error('Prepared image file missing');
|
|
2043
|
+
|
|
1822
2044
|
const isWin = process.platform === 'win32';
|
|
1823
2045
|
const isMac = process.platform === 'darwin';
|
|
1824
2046
|
const isLinux = !isWin && !isMac;
|
|
1825
|
-
if (isLinux && linuxImagePasteInFlight) {
|
|
1826
|
-
throw new Error('Another Linux image paste is still in progress. Please wait a moment and try again.');
|
|
1827
|
-
}
|
|
1828
2047
|
try {
|
|
1829
2048
|
const stat = fs.statSync(tmpFile);
|
|
1830
2049
|
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1831
|
-
if (isLinux)
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
const psCmd = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $img = [System.Drawing.Image]::FromFile('${tmpFile.replace(/'/g, "''")}'); [System.Windows.Forms.Clipboard]::SetImage($img); $img.Dispose()`;
|
|
1835
|
-
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
1836
|
-
} else if (isMac) {
|
|
1837
|
-
execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
|
|
1838
|
-
} else {
|
|
1839
|
-
const tool = await startLinuxClipboardImage(tmpFile, mediaType);
|
|
1840
|
-
log(`Linux clipboard armed with ${tool}`);
|
|
1841
|
-
}
|
|
1842
|
-
log('Clipboard set with image');
|
|
1843
|
-
|
|
1844
|
-
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
1845
|
-
await new Promise((resolve, reject) => {
|
|
1846
|
-
setTimeout(() => {
|
|
2050
|
+
if (isLinux) {
|
|
2051
|
+
const linuxPrompt = buildLinuxImagePrompt(text, tmpFile);
|
|
2052
|
+
await new Promise((resolve, reject) => {
|
|
1847
2053
|
if (!claudeProc) {
|
|
1848
|
-
reject(new Error('Claude stopped before image
|
|
2054
|
+
reject(new Error('Claude stopped before Linux image submit'));
|
|
1849
2055
|
return;
|
|
1850
2056
|
}
|
|
1851
|
-
|
|
1852
|
-
else claudeProc.write('\x16');
|
|
1853
|
-
log('Sent image paste keypress to PTY');
|
|
1854
|
-
|
|
2057
|
+
claudeProc.write(linuxPrompt);
|
|
1855
2058
|
setTimeout(() => {
|
|
1856
2059
|
if (!claudeProc) {
|
|
1857
|
-
reject(new Error('Claude stopped before image
|
|
2060
|
+
reject(new Error('Claude stopped before Linux image submit'));
|
|
1858
2061
|
return;
|
|
1859
2062
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2063
|
+
claudeProc.write('\r');
|
|
2064
|
+
log(`Sent Linux image prompt via @ref: "${linuxPrompt.substring(0, 120)}"`);
|
|
1863
2065
|
setTimeout(() => {
|
|
1864
|
-
if (
|
|
1865
|
-
|
|
1866
|
-
|
|
2066
|
+
if (onCleanup) onCleanup();
|
|
2067
|
+
else {
|
|
2068
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1867
2069
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
2070
|
+
}, LINUX_AT_IMAGE_CLEANUP_DELAY_MS);
|
|
2071
|
+
resolve();
|
|
2072
|
+
}, LINUX_AT_PROMPT_SUBMIT_DELAY_MS);
|
|
2073
|
+
});
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
1870
2076
|
|
|
1871
|
-
|
|
1872
|
-
|
|
2077
|
+
if (isWin) {
|
|
2078
|
+
const psCmd = `Add-Type -AssemblyName System.Drawing; Add-Type -AssemblyName System.Windows.Forms; $img = [System.Drawing.Image]::FromFile('${tmpFile.replace(/'/g, "''")}'); [System.Windows.Forms.Clipboard]::SetImage($img); $img.Dispose()`;
|
|
2079
|
+
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
2080
|
+
} else if (isMac) {
|
|
2081
|
+
execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
|
|
2082
|
+
}
|
|
2083
|
+
log('Clipboard set with image');
|
|
2084
|
+
|
|
2085
|
+
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
2086
|
+
await new Promise((resolve, reject) => {
|
|
2087
|
+
setTimeout(() => {
|
|
2088
|
+
if (!claudeProc) {
|
|
2089
|
+
reject(new Error('Claude stopped before image paste'));
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (isWin) claudeProc.write('\x1bv');
|
|
2093
|
+
else claudeProc.write('\x16');
|
|
2094
|
+
log('Sent image paste keypress to PTY');
|
|
2095
|
+
|
|
2096
|
+
setTimeout(() => {
|
|
2097
|
+
if (!claudeProc) {
|
|
2098
|
+
reject(new Error('Claude stopped before image prompt'));
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
const trimmedText = (text || '').trim();
|
|
2102
|
+
if (trimmedText) claudeProc.write(trimmedText);
|
|
2103
|
+
|
|
2104
|
+
setTimeout(() => {
|
|
2105
|
+
if (!claudeProc) {
|
|
2106
|
+
reject(new Error('Claude stopped before image submit'));
|
|
2107
|
+
return;
|
|
1873
2108
|
}
|
|
2109
|
+
claudeProc.write('\r');
|
|
2110
|
+
log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
|
|
1874
2111
|
|
|
1875
2112
|
setTimeout(() => {
|
|
1876
|
-
if (isLinux) setLinuxImagePasteInFlight(false, 'cleanup');
|
|
1877
2113
|
if (onCleanup) onCleanup();
|
|
1878
2114
|
else {
|
|
1879
2115
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1880
2116
|
}
|
|
1881
|
-
}, 5000);
|
|
1882
|
-
resolve();
|
|
1883
|
-
}, 150);
|
|
1884
|
-
}, 1000);
|
|
1885
|
-
}, pasteDelayMs);
|
|
1886
|
-
});
|
|
2117
|
+
}, 5000);
|
|
2118
|
+
resolve();
|
|
2119
|
+
}, 150);
|
|
2120
|
+
}, 1000);
|
|
2121
|
+
}, pasteDelayMs);
|
|
2122
|
+
});
|
|
1887
2123
|
} catch (err) {
|
|
1888
2124
|
log(`Image upload error: ${err.message}`);
|
|
1889
|
-
if (isLinux) {
|
|
1890
|
-
clearActiveLinuxClipboardProc('error');
|
|
1891
|
-
setLinuxImagePasteInFlight(false, 'error');
|
|
1892
|
-
}
|
|
1893
2125
|
if (onCleanup) onCleanup();
|
|
1894
2126
|
else {
|
|
1895
2127
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -1907,30 +2139,26 @@ function handleImageUpload(msg) {
|
|
|
1907
2139
|
log('Image upload ignored: no base64 data');
|
|
1908
2140
|
return;
|
|
1909
2141
|
}
|
|
1910
|
-
let tmpFile = null;
|
|
1911
|
-
|
|
2142
|
+
let tmpFile = null;
|
|
2143
|
+
|
|
1912
2144
|
try {
|
|
1913
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1914
|
-
const tools = assertLinuxClipboardAvailable();
|
|
1915
|
-
log(`Linux clipboard preflight OK (legacy upload): ${tools.join(', ')}`);
|
|
1916
|
-
}
|
|
1917
2145
|
const buf = Buffer.from(msg.base64, 'base64');
|
|
1918
2146
|
tmpFile = createTempImageFile(buf, msg.mediaType, `legacy_${Date.now()}`);
|
|
1919
2147
|
log(`Image saved: ${tmpFile} (${buf.length} bytes)`);
|
|
1920
|
-
handlePreparedImageUpload({
|
|
1921
|
-
tmpFile,
|
|
1922
|
-
mediaType: msg.mediaType,
|
|
1923
|
-
text: msg.text || '',
|
|
1924
|
-
}).then(() => {
|
|
1925
|
-
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1926
|
-
}).catch((err) => {
|
|
1927
|
-
log(`Image upload error: ${err.message}`);
|
|
1928
|
-
});
|
|
1929
|
-
} catch (err) {
|
|
1930
|
-
log(`Image upload error: ${err.message}`);
|
|
1931
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
2148
|
+
handlePreparedImageUpload({
|
|
2149
|
+
tmpFile,
|
|
2150
|
+
mediaType: msg.mediaType,
|
|
2151
|
+
text: msg.text || '',
|
|
2152
|
+
}).then(() => {
|
|
2153
|
+
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
2154
|
+
}).catch((err) => {
|
|
2155
|
+
log(`Image upload error: ${err.message}`);
|
|
2156
|
+
});
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
log(`Image upload error: ${err.message}`);
|
|
2159
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
1934
2162
|
|
|
1935
2163
|
// ============================================================
|
|
1936
2164
|
// 6. Hook Auto-Setup
|