claude-remote 0.4.6 → 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 -232
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,136 +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}`);
|
|
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;
|
|
715
698
|
}
|
|
716
699
|
|
|
717
|
-
function
|
|
718
|
-
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
return
|
|
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('/');
|
|
723
706
|
}
|
|
724
707
|
|
|
725
|
-
function
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
if (!process.env.WAYLAND_DISPLAY && !process.env.DISPLAY) {
|
|
730
|
-
throw new Error('Linux image paste requires an active graphical session (WAYLAND_DISPLAY or DISPLAY).');
|
|
731
|
-
}
|
|
732
|
-
throw new Error('Linux image paste requires wl-copy or xclip on the server. Install a matching clipboard tool and try again.');
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function spawnLinuxClipboardTool(tool, imageBuffer, type) {
|
|
736
|
-
return new Promise((resolve, reject) => {
|
|
737
|
-
const args = tool === 'xclip'
|
|
738
|
-
? ['-selection', 'clipboard', '-t', type, '-i']
|
|
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
|
-
try { child.kill('SIGTERM'); } catch {}
|
|
753
|
-
reject(new Error(message));
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
const settleSuccess = () => {
|
|
757
|
-
if (settled) return;
|
|
758
|
-
settled = true;
|
|
759
|
-
if (readyTimer) clearTimeout(readyTimer);
|
|
760
|
-
activeLinuxClipboardProc = { child, tool };
|
|
761
|
-
child.unref();
|
|
762
|
-
resolve(tool);
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
child.on('error', (err) => {
|
|
766
|
-
log(`Linux clipboard process error (${tool}): ${err.message}`);
|
|
767
|
-
settleFailure(`Linux clipboard tool ${tool} failed: ${err.message}`);
|
|
768
|
-
});
|
|
769
|
-
child.stderr.on('data', (chunk) => {
|
|
770
|
-
stderr += chunk.toString('utf8');
|
|
771
|
-
if (stderr.length > 2000) stderr = stderr.slice(-2000);
|
|
772
|
-
});
|
|
773
|
-
child.on('exit', (code, signal) => {
|
|
774
|
-
if (activeLinuxClipboardProc && activeLinuxClipboardProc.child === child) activeLinuxClipboardProc = null;
|
|
775
|
-
const extra = stderr.trim() ? ` stderr=${JSON.stringify(stderr.trim())}` : '';
|
|
776
|
-
log(`Linux clipboard process exited (${tool}) code=${code ?? 'null'} signal=${signal ?? 'null'}${extra}`);
|
|
777
|
-
if (!settled) {
|
|
778
|
-
const detail = stderr.trim() || `exit code ${code ?? 'null'} signal ${signal ?? 'null'}`;
|
|
779
|
-
settleFailure(`Linux clipboard tool ${tool} exited before paste: ${detail}`);
|
|
780
|
-
}
|
|
781
|
-
});
|
|
782
|
-
child.stdin.on('error', (err) => {
|
|
783
|
-
if (err.code === 'EPIPE') {
|
|
784
|
-
settleFailure(`Linux clipboard tool ${tool} closed its input early`);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
log(`Linux clipboard stdin error (${tool}): ${err.message}`);
|
|
788
|
-
settleFailure(`Linux clipboard tool ${tool} stdin failed: ${err.message}`);
|
|
789
|
-
});
|
|
790
|
-
|
|
791
|
-
child.stdin.end(imageBuffer);
|
|
792
|
-
log(`Linux clipboard process started (${tool}) pid=${child.pid ?? 'null'} type=${type} bytes=${imageBuffer.length}`);
|
|
793
|
-
readyTimer = setTimeout(() => settleSuccess(), LINUX_CLIPBOARD_READY_GRACE_MS);
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
async function startLinuxClipboardImage(tmpFile, mediaType) {
|
|
798
|
-
const type = String(mediaType || 'image/png').toLowerCase();
|
|
799
|
-
const imageBuffer = fs.readFileSync(tmpFile);
|
|
800
|
-
const availableTools = assertLinuxClipboardAvailable();
|
|
801
|
-
clearActiveLinuxClipboardProc('replace');
|
|
802
|
-
|
|
803
|
-
let lastErr = null;
|
|
804
|
-
for (const tool of availableTools) {
|
|
805
|
-
try {
|
|
806
|
-
return await spawnLinuxClipboardTool(tool, imageBuffer, type);
|
|
807
|
-
} catch (err) {
|
|
808
|
-
lastErr = err;
|
|
809
|
-
log(`Linux clipboard arm failed (${tool}): ${err.message}`);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
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;
|
|
814
712
|
}
|
|
815
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
|
+
|
|
816
1043
|
setInterval(() => {
|
|
817
1044
|
const now = Date.now();
|
|
818
1045
|
for (const [uploadId, upload] of pendingImageUploads) {
|
|
@@ -1055,26 +1282,17 @@ wss.on('connection', (ws, req) => {
|
|
|
1055
1282
|
}
|
|
1056
1283
|
break;
|
|
1057
1284
|
}
|
|
1058
|
-
case 'image_upload_init': {
|
|
1059
|
-
const uploadId = String(msg.uploadId || '');
|
|
1060
|
-
if (!uploadId) {
|
|
1061
|
-
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1062
|
-
break;
|
|
1063
|
-
}
|
|
1064
|
-
cleanupImageUpload(uploadId);
|
|
1065
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1066
|
-
try {
|
|
1067
|
-
const tools = assertLinuxClipboardAvailable();
|
|
1068
|
-
log(`Linux clipboard preflight OK: ${tools.join(', ')}`);
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
sendUploadStatus(ws, uploadId, 'error', { message: err.message });
|
|
1071
|
-
break;
|
|
1072
|
-
}
|
|
1285
|
+
case 'image_upload_init': {
|
|
1286
|
+
const uploadId = String(msg.uploadId || '');
|
|
1287
|
+
if (!uploadId) {
|
|
1288
|
+
sendUploadStatus(ws, '', 'error', { message: 'Missing uploadId' });
|
|
1289
|
+
break;
|
|
1073
1290
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1291
|
+
cleanupImageUpload(uploadId);
|
|
1292
|
+
pendingImageUploads.set(uploadId, {
|
|
1293
|
+
id: uploadId,
|
|
1294
|
+
owner: ws,
|
|
1295
|
+
mediaType: msg.mediaType || 'image/png',
|
|
1078
1296
|
name: msg.name || 'image',
|
|
1079
1297
|
totalBytes: Number.isFinite(msg.totalBytes) ? msg.totalBytes : 0,
|
|
1080
1298
|
totalChunks: Number.isFinite(msg.totalChunks) ? msg.totalChunks : 0,
|
|
@@ -1172,13 +1390,13 @@ wss.on('connection', (ws, req) => {
|
|
|
1172
1390
|
sendUploadStatus(ws, uploadId, 'error', { message: 'Upload not ready' });
|
|
1173
1391
|
break;
|
|
1174
1392
|
}
|
|
1175
|
-
try {
|
|
1176
|
-
await handlePreparedImageUpload({
|
|
1177
|
-
tmpFile: upload.tmpFile,
|
|
1178
|
-
mediaType: upload.mediaType,
|
|
1179
|
-
text: msg.text || '',
|
|
1180
|
-
logLabel: upload.name || uploadId,
|
|
1181
|
-
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),
|
|
1182
1400
|
});
|
|
1183
1401
|
upload.submitted = true;
|
|
1184
1402
|
upload.updatedAt = Date.now();
|
|
@@ -1387,11 +1605,20 @@ function extractSlashCommand(content) {
|
|
|
1387
1605
|
return inlineMatch ? inlineMatch[1].trim().toLowerCase() : '';
|
|
1388
1606
|
}
|
|
1389
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
|
+
|
|
1390
1616
|
function isNonAiUserEvent(event, content) {
|
|
1391
1617
|
if (!event || typeof event !== 'object') return false;
|
|
1392
1618
|
if (event.isMeta === true) return true;
|
|
1393
1619
|
if (event.isCompactSummary === true) return true;
|
|
1394
1620
|
if (event.isVisibleInTranscriptOnly === true) return true;
|
|
1621
|
+
if (isUserInterruptEvent(content)) return true;
|
|
1395
1622
|
|
|
1396
1623
|
const text = flattenUserContent(content).trim();
|
|
1397
1624
|
if (!text) return false;
|
|
@@ -1545,12 +1772,16 @@ function startTailing() {
|
|
|
1545
1772
|
if (event.type === 'user' || (event.message && event.message.role === 'user')) {
|
|
1546
1773
|
const content = event.message && event.message.content;
|
|
1547
1774
|
const slashCommand = extractSlashCommand(content);
|
|
1775
|
+
const isInterruptedUserEvent = isUserInterruptEvent(content);
|
|
1548
1776
|
const isPassiveUserEvent = isNonAiUserEvent(event, content);
|
|
1549
1777
|
const ignoreInitialClear = (
|
|
1550
1778
|
slashCommand === '/clear' &&
|
|
1551
1779
|
pendingInitialClearTranscript &&
|
|
1552
1780
|
pendingInitialClearTranscript.sessionId === currentSessionId
|
|
1553
1781
|
);
|
|
1782
|
+
if (!tailCatchingUp && isInterruptedUserEvent) {
|
|
1783
|
+
setTurnState('idle', { sessionId: currentSessionId, reason: 'transcript_user_interrupt' });
|
|
1784
|
+
}
|
|
1554
1785
|
// Only live, AI-producing user messages can move the turn state
|
|
1555
1786
|
// into running. Historical replay and slash commands are ignored.
|
|
1556
1787
|
if (!tailCatchingUp && !slashCommand && !isPassiveUserEvent) {
|
|
@@ -1806,81 +2037,91 @@ function restartClaude(newCwd) {
|
|
|
1806
2037
|
// ============================================================
|
|
1807
2038
|
// 5. Image Upload → Clipboard Injection
|
|
1808
2039
|
// ============================================================
|
|
1809
|
-
async function handlePreparedImageUpload({ tmpFile, mediaType, text, logLabel = '', onCleanup = null }) {
|
|
1810
|
-
if (!claudeProc) throw new Error('Claude not running');
|
|
1811
|
-
if (!tmpFile || !fs.existsSync(tmpFile)) throw new Error('Prepared image file missing');
|
|
1812
|
-
|
|
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
|
+
|
|
1813
2044
|
const isWin = process.platform === 'win32';
|
|
1814
2045
|
const isMac = process.platform === 'darwin';
|
|
1815
2046
|
const isLinux = !isWin && !isMac;
|
|
1816
|
-
if (isLinux && linuxImagePasteInFlight) {
|
|
1817
|
-
throw new Error('Another Linux image paste is still in progress. Please wait a moment and try again.');
|
|
1818
|
-
}
|
|
1819
2047
|
try {
|
|
1820
2048
|
const stat = fs.statSync(tmpFile);
|
|
1821
2049
|
log(`Image ready: ${logLabel || path.basename(tmpFile)} (${stat.size} bytes)`);
|
|
1822
|
-
if (isLinux)
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
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()`;
|
|
1826
|
-
execSync(`powershell -NoProfile -STA -Command "${psCmd}"`, { timeout: 10000 });
|
|
1827
|
-
} else if (isMac) {
|
|
1828
|
-
execSync(`osascript -e 'set the clipboard to (read POSIX file "${tmpFile}" as 芦class PNGf禄)'`, { timeout: 10000 });
|
|
1829
|
-
} else {
|
|
1830
|
-
const tool = await startLinuxClipboardImage(tmpFile, mediaType);
|
|
1831
|
-
log(`Linux clipboard armed with ${tool}`);
|
|
1832
|
-
}
|
|
1833
|
-
log('Clipboard set with image');
|
|
1834
|
-
|
|
1835
|
-
const pasteDelayMs = isWin || isMac ? 0 : 150;
|
|
1836
|
-
await new Promise((resolve, reject) => {
|
|
1837
|
-
setTimeout(() => {
|
|
2050
|
+
if (isLinux) {
|
|
2051
|
+
const linuxPrompt = buildLinuxImagePrompt(text, tmpFile);
|
|
2052
|
+
await new Promise((resolve, reject) => {
|
|
1838
2053
|
if (!claudeProc) {
|
|
1839
|
-
reject(new Error('Claude stopped before image
|
|
2054
|
+
reject(new Error('Claude stopped before Linux image submit'));
|
|
1840
2055
|
return;
|
|
1841
2056
|
}
|
|
1842
|
-
|
|
1843
|
-
else claudeProc.write('\x16');
|
|
1844
|
-
log('Sent image paste keypress to PTY');
|
|
1845
|
-
|
|
2057
|
+
claudeProc.write(linuxPrompt);
|
|
1846
2058
|
setTimeout(() => {
|
|
1847
2059
|
if (!claudeProc) {
|
|
1848
|
-
reject(new Error('Claude stopped before image
|
|
2060
|
+
reject(new Error('Claude stopped before Linux image submit'));
|
|
1849
2061
|
return;
|
|
1850
2062
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2063
|
+
claudeProc.write('\r');
|
|
2064
|
+
log(`Sent Linux image prompt via @ref: "${linuxPrompt.substring(0, 120)}"`);
|
|
1854
2065
|
setTimeout(() => {
|
|
1855
|
-
if (
|
|
1856
|
-
|
|
1857
|
-
|
|
2066
|
+
if (onCleanup) onCleanup();
|
|
2067
|
+
else {
|
|
2068
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1858
2069
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
2070
|
+
}, LINUX_AT_IMAGE_CLEANUP_DELAY_MS);
|
|
2071
|
+
resolve();
|
|
2072
|
+
}, LINUX_AT_PROMPT_SUBMIT_DELAY_MS);
|
|
2073
|
+
});
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
1861
2076
|
|
|
1862
|
-
|
|
1863
|
-
|
|
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;
|
|
1864
2108
|
}
|
|
2109
|
+
claudeProc.write('\r');
|
|
2110
|
+
log('Sent Enter after image paste' + (trimmedText ? ` + text: "${trimmedText.substring(0, 60)}"` : ''));
|
|
1865
2111
|
|
|
1866
2112
|
setTimeout(() => {
|
|
1867
|
-
if (isLinux) setLinuxImagePasteInFlight(false, 'cleanup');
|
|
1868
2113
|
if (onCleanup) onCleanup();
|
|
1869
2114
|
else {
|
|
1870
2115
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1871
2116
|
}
|
|
1872
|
-
}, 5000);
|
|
1873
|
-
resolve();
|
|
1874
|
-
}, 150);
|
|
1875
|
-
}, 1000);
|
|
1876
|
-
}, pasteDelayMs);
|
|
1877
|
-
});
|
|
2117
|
+
}, 5000);
|
|
2118
|
+
resolve();
|
|
2119
|
+
}, 150);
|
|
2120
|
+
}, 1000);
|
|
2121
|
+
}, pasteDelayMs);
|
|
2122
|
+
});
|
|
1878
2123
|
} catch (err) {
|
|
1879
2124
|
log(`Image upload error: ${err.message}`);
|
|
1880
|
-
if (isLinux) {
|
|
1881
|
-
clearActiveLinuxClipboardProc('error');
|
|
1882
|
-
setLinuxImagePasteInFlight(false, 'error');
|
|
1883
|
-
}
|
|
1884
2125
|
if (onCleanup) onCleanup();
|
|
1885
2126
|
else {
|
|
1886
2127
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -1898,30 +2139,26 @@ function handleImageUpload(msg) {
|
|
|
1898
2139
|
log('Image upload ignored: no base64 data');
|
|
1899
2140
|
return;
|
|
1900
2141
|
}
|
|
1901
|
-
let tmpFile = null;
|
|
1902
|
-
|
|
2142
|
+
let tmpFile = null;
|
|
2143
|
+
|
|
1903
2144
|
try {
|
|
1904
|
-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
|
|
1905
|
-
const tools = assertLinuxClipboardAvailable();
|
|
1906
|
-
log(`Linux clipboard preflight OK (legacy upload): ${tools.join(', ')}`);
|
|
1907
|
-
}
|
|
1908
2145
|
const buf = Buffer.from(msg.base64, 'base64');
|
|
1909
2146
|
tmpFile = createTempImageFile(buf, msg.mediaType, `legacy_${Date.now()}`);
|
|
1910
2147
|
log(`Image saved: ${tmpFile} (${buf.length} bytes)`);
|
|
1911
|
-
handlePreparedImageUpload({
|
|
1912
|
-
tmpFile,
|
|
1913
|
-
mediaType: msg.mediaType,
|
|
1914
|
-
text: msg.text || '',
|
|
1915
|
-
}).then(() => {
|
|
1916
|
-
setTurnState('running', { reason: 'legacy_image_upload' });
|
|
1917
|
-
}).catch((err) => {
|
|
1918
|
-
log(`Image upload error: ${err.message}`);
|
|
1919
|
-
});
|
|
1920
|
-
} catch (err) {
|
|
1921
|
-
log(`Image upload error: ${err.message}`);
|
|
1922
|
-
try { fs.unlinkSync(tmpFile); } catch {}
|
|
1923
|
-
}
|
|
1924
|
-
}
|
|
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
|
+
}
|
|
1925
2162
|
|
|
1926
2163
|
// ============================================================
|
|
1927
2164
|
// 6. Hook Auto-Setup
|