mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-02-12 19:57:34 +00:00
Compare commits
5 Commits
v1.16.4
...
fix/claude
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa0d187a5e | ||
|
|
db2352114f | ||
|
|
adb671f28d | ||
|
|
5104117af6 | ||
|
|
a19a1709d3 |
131
server/index.js
131
server/index.js
@@ -178,6 +178,69 @@ const server = http.createServer(app);
|
|||||||
|
|
||||||
const ptySessionsMap = new Map();
|
const ptySessionsMap = new Map();
|
||||||
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
||||||
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
||||||
|
const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
||||||
|
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
||||||
|
|
||||||
|
function stripAnsiSequences(value = '') {
|
||||||
|
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDetectedUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') return null;
|
||||||
|
|
||||||
|
const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
||||||
|
if (!cleaned) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(cleaned);
|
||||||
|
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed.toString();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractUrlsFromText(value = '') {
|
||||||
|
const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
|
||||||
|
|
||||||
|
// Handle wrapped terminal URLs split across lines by terminal width.
|
||||||
|
const wrappedMatches = [];
|
||||||
|
const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
|
||||||
|
const lines = value.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
|
||||||
|
if (!startMatch) continue;
|
||||||
|
|
||||||
|
let combined = startMatch[0];
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < lines.length) {
|
||||||
|
const continuation = lines[j].trim();
|
||||||
|
if (!continuation) break;
|
||||||
|
if (!continuationRegex.test(continuation)) break;
|
||||||
|
combined += continuation;
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAutoOpenUrlFromOutput(value = '') {
|
||||||
|
const normalized = value.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.includes('browser didn\'t open') ||
|
||||||
|
normalized.includes('open this url') ||
|
||||||
|
normalized.includes('continue in your browser') ||
|
||||||
|
normalized.includes('press enter to open') ||
|
||||||
|
normalized.includes('open_url:')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Single WebSocket server that handles both paths
|
// Single WebSocket server that handles both paths
|
||||||
const wss = new WebSocketServer({
|
const wss = new WebSocketServer({
|
||||||
@@ -960,7 +1023,8 @@ function handleShellConnection(ws) {
|
|||||||
console.log('🐚 Shell client connected');
|
console.log('🐚 Shell client connected');
|
||||||
let shellProcess = null;
|
let shellProcess = null;
|
||||||
let ptySessionKey = null;
|
let ptySessionKey = null;
|
||||||
let outputBuffer = [];
|
let urlDetectionBuffer = '';
|
||||||
|
const announcedAuthUrls = new Set();
|
||||||
|
|
||||||
ws.on('message', async (message) => {
|
ws.on('message', async (message) => {
|
||||||
try {
|
try {
|
||||||
@@ -974,6 +1038,8 @@ function handleShellConnection(ws) {
|
|||||||
const provider = data.provider || 'claude';
|
const provider = data.provider || 'claude';
|
||||||
const initialCommand = data.initialCommand;
|
const initialCommand = data.initialCommand;
|
||||||
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
||||||
|
urlDetectionBuffer = '';
|
||||||
|
announcedAuthUrls.clear();
|
||||||
|
|
||||||
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
||||||
const isLoginCommand = initialCommand && (
|
const isLoginCommand = initialCommand && (
|
||||||
@@ -1113,9 +1179,7 @@ function handleShellConnection(ws) {
|
|||||||
...process.env,
|
...process.env,
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
COLORTERM: 'truecolor',
|
COLORTERM: 'truecolor',
|
||||||
FORCE_COLOR: '3',
|
FORCE_COLOR: '3'
|
||||||
// Override browser opening commands to echo URL for detection
|
|
||||||
BROWSER: os.platform() === 'win32' ? 'echo "OPEN_URL:"' : 'echo "OPEN_URL:"'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1145,38 +1209,47 @@ function handleShellConnection(ws) {
|
|||||||
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
||||||
let outputData = data;
|
let outputData = data;
|
||||||
|
|
||||||
// Check for various URL opening patterns
|
const cleanChunk = stripAnsiSequences(data);
|
||||||
const patterns = [
|
urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
|
||||||
// Direct browser opening commands
|
|
||||||
/(?:xdg-open|open|start)\s+(https?:\/\/[^\s\x1b\x07]+)/g,
|
outputData = outputData.replace(
|
||||||
// BROWSER environment variable override
|
|
||||||
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
||||||
// Git and other tools opening URLs
|
'[INFO] Opening in browser: $1'
|
||||||
/Opening\s+(https?:\/\/[^\s\x1b\x07]+)/gi,
|
);
|
||||||
// General URL patterns that might be opened
|
|
||||||
/Visit:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
|
|
||||||
/View at:\s*(https?:\/\/[^\s\x1b\x07]+)/gi,
|
|
||||||
/Browse to:\s*(https?:\/\/[^\s\x1b\x07]+)/gi
|
|
||||||
];
|
|
||||||
|
|
||||||
patterns.forEach(pattern => {
|
const emitAuthUrl = (detectedUrl, autoOpen = false) => {
|
||||||
let match;
|
const normalizedUrl = normalizeDetectedUrl(detectedUrl);
|
||||||
while ((match = pattern.exec(data)) !== null) {
|
if (!normalizedUrl) return;
|
||||||
const url = match[1];
|
|
||||||
console.log('[DEBUG] Detected URL for opening:', url);
|
|
||||||
|
|
||||||
// Send URL opening message to client
|
const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
|
||||||
|
if (isNewUrl) {
|
||||||
|
announcedAuthUrls.add(normalizedUrl);
|
||||||
session.ws.send(JSON.stringify({
|
session.ws.send(JSON.stringify({
|
||||||
type: 'url_open',
|
type: 'auth_url',
|
||||||
url: url
|
url: normalizedUrl,
|
||||||
|
autoOpen
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Replace the OPEN_URL pattern with a user-friendly message
|
};
|
||||||
if (pattern.source.includes('OPEN_URL')) {
|
|
||||||
outputData = outputData.replace(match[0], `[INFO] Opening in browser: ${url}`);
|
const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
|
||||||
|
.map((url) => normalizeDetectedUrl(url))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Prefer the most complete URL if shorter prefix variants are also present.
|
||||||
|
const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
|
||||||
|
!urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
|
||||||
|
);
|
||||||
|
|
||||||
|
dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
|
||||||
|
|
||||||
|
if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
|
||||||
|
const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
|
||||||
|
current.length > longest.length ? current : longest
|
||||||
|
);
|
||||||
|
emitAuthUrl(bestUrl, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send regular output
|
// Send regular output
|
||||||
session.ws.send(JSON.stringify({
|
session.ws.send(JSON.stringify({
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ function LoginModal({
|
|||||||
if (onComplete) {
|
if (onComplete) {
|
||||||
onComplete(exitCode);
|
onComplete(exitCode);
|
||||||
}
|
}
|
||||||
if (exitCode === 0) {
|
// Keep modal open so users can read login output and close explicitly.
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -26,6 +26,31 @@ if (typeof document !== 'undefined') {
|
|||||||
document.head.appendChild(styleSheet);
|
document.head.appendChild(styleSheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackCopyToClipboard(text) {
|
||||||
|
if (!text || typeof document === 'undefined') return false;
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute('readonly', '');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
textarea.style.pointerEvents = 'none';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.focus();
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
try {
|
||||||
|
copied = document.execCommand('copy');
|
||||||
|
} catch {
|
||||||
|
copied = false;
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copied;
|
||||||
|
}
|
||||||
|
|
||||||
function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) {
|
function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) {
|
||||||
const { t } = useTranslation('chat');
|
const { t } = useTranslation('chat');
|
||||||
const terminalRef = useRef(null);
|
const terminalRef = useRef(null);
|
||||||
@@ -37,12 +62,15 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
const [isRestarting, setIsRestarting] = useState(false);
|
const [isRestarting, setIsRestarting] = useState(false);
|
||||||
const [lastSessionId, setLastSessionId] = useState(null);
|
const [lastSessionId, setLastSessionId] = useState(null);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
const [isConnecting, setIsConnecting] = useState(false);
|
||||||
|
const [authUrl, setAuthUrl] = useState('');
|
||||||
|
const [authUrlCopyStatus, setAuthUrlCopyStatus] = useState('idle');
|
||||||
|
|
||||||
const selectedProjectRef = useRef(selectedProject);
|
const selectedProjectRef = useRef(selectedProject);
|
||||||
const selectedSessionRef = useRef(selectedSession);
|
const selectedSessionRef = useRef(selectedSession);
|
||||||
const initialCommandRef = useRef(initialCommand);
|
const initialCommandRef = useRef(initialCommand);
|
||||||
const isPlainShellRef = useRef(isPlainShell);
|
const isPlainShellRef = useRef(isPlainShell);
|
||||||
const onProcessCompleteRef = useRef(onProcessComplete);
|
const onProcessCompleteRef = useRef(onProcessComplete);
|
||||||
|
const authUrlRef = useRef('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
selectedProjectRef.current = selectedProject;
|
selectedProjectRef.current = selectedProject;
|
||||||
@@ -52,6 +80,42 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
onProcessCompleteRef.current = onProcessComplete;
|
onProcessCompleteRef.current = onProcessComplete;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openAuthUrlInBrowser = useCallback((url = authUrlRef.current) => {
|
||||||
|
if (!url) return false;
|
||||||
|
|
||||||
|
const popup = window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
if (popup) {
|
||||||
|
try {
|
||||||
|
popup.opener = null;
|
||||||
|
} catch {
|
||||||
|
// Ignore cross-origin restrictions when trying to null opener
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const copyAuthUrlToClipboard = useCallback(async (url = authUrlRef.current) => {
|
||||||
|
if (!url) return false;
|
||||||
|
|
||||||
|
let copied = false;
|
||||||
|
try {
|
||||||
|
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
copied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!copied) {
|
||||||
|
copied = fallbackCopyToClipboard(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return copied;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const connectWebSocket = useCallback(async () => {
|
const connectWebSocket = useCallback(async () => {
|
||||||
if (isConnecting || isConnected) return;
|
if (isConnecting || isConnected) return;
|
||||||
|
|
||||||
@@ -77,6 +141,9 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
ws.current.onopen = () => {
|
ws.current.onopen = () => {
|
||||||
setIsConnected(true);
|
setIsConnected(true);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
authUrlRef.current = '';
|
||||||
|
setAuthUrl('');
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (fitAddon.current && terminal.current) {
|
if (fitAddon.current && terminal.current) {
|
||||||
@@ -119,8 +186,16 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
if (terminal.current) {
|
if (terminal.current) {
|
||||||
terminal.current.write(output);
|
terminal.current.write(output);
|
||||||
}
|
}
|
||||||
|
} else if (data.type === 'auth_url' && data.url) {
|
||||||
|
authUrlRef.current = data.url;
|
||||||
|
setAuthUrl(data.url);
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
} else if (data.type === 'url_open') {
|
} else if (data.type === 'url_open') {
|
||||||
window.open(data.url, '_blank');
|
if (data.url) {
|
||||||
|
authUrlRef.current = data.url;
|
||||||
|
setAuthUrl(data.url);
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Shell] Error handling WebSocket message:', error, event.data);
|
console.error('[Shell] Error handling WebSocket message:', error, event.data);
|
||||||
@@ -130,6 +205,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
ws.current.onclose = (event) => {
|
ws.current.onclose = (event) => {
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
|
|
||||||
if (terminal.current) {
|
if (terminal.current) {
|
||||||
terminal.current.clear();
|
terminal.current.clear();
|
||||||
@@ -145,7 +221,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
}, [isConnecting, isConnected]);
|
}, [isConnecting, isConnected, openAuthUrlInBrowser]);
|
||||||
|
|
||||||
const connectToShell = useCallback(() => {
|
const connectToShell = useCallback(() => {
|
||||||
if (!isInitialized || isConnected || isConnecting) return;
|
if (!isInitialized || isConnected || isConnecting) return;
|
||||||
@@ -166,6 +242,9 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
|
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
authUrlRef.current = '';
|
||||||
|
setAuthUrl('');
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sessionDisplayName = useMemo(() => {
|
const sessionDisplayName = useMemo(() => {
|
||||||
@@ -201,6 +280,9 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
|
|
||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setIsInitialized(false);
|
setIsInitialized(false);
|
||||||
|
authUrlRef.current = '';
|
||||||
|
setAuthUrl('');
|
||||||
|
setAuthUrlCopyStatus('idle');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsRestarting(false);
|
setIsRestarting(false);
|
||||||
@@ -272,7 +354,10 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
const webLinksAddon = new WebLinksAddon();
|
const webLinksAddon = new WebLinksAddon();
|
||||||
|
|
||||||
terminal.current.loadAddon(fitAddon.current);
|
terminal.current.loadAddon(fitAddon.current);
|
||||||
|
// Disable xterm link auto-detection in minimal (login) mode to avoid partial wrapped URL links.
|
||||||
|
if (!minimal) {
|
||||||
terminal.current.loadAddon(webLinksAddon);
|
terminal.current.loadAddon(webLinksAddon);
|
||||||
|
}
|
||||||
// Note: ClipboardAddon removed - we handle clipboard operations manually in attachCustomKeyEventHandler
|
// Note: ClipboardAddon removed - we handle clipboard operations manually in attachCustomKeyEventHandler
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -284,12 +369,41 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
terminal.current.open(terminalRef.current);
|
terminal.current.open(terminalRef.current);
|
||||||
|
|
||||||
terminal.current.attachCustomKeyEventHandler((event) => {
|
terminal.current.attachCustomKeyEventHandler((event) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === 'c' && terminal.current.hasSelection()) {
|
if (
|
||||||
|
event.type === 'keydown' &&
|
||||||
|
minimal &&
|
||||||
|
isPlainShellRef.current &&
|
||||||
|
authUrlRef.current &&
|
||||||
|
!event.ctrlKey &&
|
||||||
|
!event.metaKey &&
|
||||||
|
!event.altKey &&
|
||||||
|
event.key?.toLowerCase() === 'c'
|
||||||
|
) {
|
||||||
|
copyAuthUrlToClipboard(authUrlRef.current).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event.type === 'keydown' &&
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.key?.toLowerCase() === 'c' &&
|
||||||
|
terminal.current.hasSelection()
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
if (
|
||||||
|
event.type === 'keydown' &&
|
||||||
|
(event.ctrlKey || event.metaKey) &&
|
||||||
|
event.key?.toLowerCase() === 'v'
|
||||||
|
) {
|
||||||
|
// Block native browser/xterm paste so clipboard data is only sent after
|
||||||
|
// the explicit clipboard-read flow resolves (avoids duplicate pastes).
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
navigator.clipboard.readText().then(text => {
|
navigator.clipboard.readText().then(text => {
|
||||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||||
ws.current.send(JSON.stringify({
|
ws.current.send(JSON.stringify({
|
||||||
@@ -359,7 +473,7 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
terminal.current = null;
|
terminal.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [selectedProject?.path || selectedProject?.fullPath, isRestarting]);
|
}, [selectedProject?.path || selectedProject?.fullPath, isRestarting, minimal, copyAuthUrlToClipboard]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoConnect || !isInitialized || isConnecting || isConnected) return;
|
if (!autoConnect || !isInitialized || isConnecting || isConnected) return;
|
||||||
@@ -383,9 +497,47 @@ function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (minimal) {
|
if (minimal) {
|
||||||
|
const hasAuthUrl = Boolean(authUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full bg-gray-900">
|
<div className="h-full w-full bg-gray-900 relative">
|
||||||
<div ref={terminalRef} className="h-full w-full focus:outline-none" style={{ outline: 'none' }} />
|
<div ref={terminalRef} className="h-full w-full focus:outline-none" style={{ outline: 'none' }} />
|
||||||
|
{hasAuthUrl && (
|
||||||
|
<div className="absolute inset-x-0 bottom-14 z-20 border-t border-gray-700/80 bg-gray-900/95 p-3 backdrop-blur-sm">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-xs text-gray-300">Open or copy the login URL:</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={authUrl}
|
||||||
|
readOnly
|
||||||
|
onClick={(event) => event.currentTarget.select()}
|
||||||
|
className="w-full rounded border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||||
|
aria-label="Authentication URL"
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
openAuthUrlInBrowser(authUrl);
|
||||||
|
}}
|
||||||
|
className="flex-1 rounded bg-blue-600 px-3 py-2 text-xs font-medium text-white hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Open URL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
const copied = await copyAuthUrlToClipboard(authUrl);
|
||||||
|
setAuthUrlCopyStatus(copied ? 'copied' : 'failed');
|
||||||
|
}}
|
||||||
|
className="flex-1 rounded bg-gray-700 px-3 py-2 text-xs font-medium text-white hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
{authUrlCopyStatus === 'copied' ? 'Copied' : 'Copy URL'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user