mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,25 +1,21 @@
|
||||
import { useState, useEffect, useLayoutEffect, useCallback, useRef } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "./ui/button";
|
||||
import { StatusDot } from "./ui/connection-status";
|
||||
import { ThemeToggle } from "./ui/theme-toggle";
|
||||
import { Label } from "./ui/label";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupInput,
|
||||
} from "./ui/input-group";
|
||||
import { ACPClient, DEFAULT_SETTINGS, DisconnectRequestedError } from "../src/acp";
|
||||
import type { ACPSettings, ConnectionState, BrowserToolParams, BrowserToolResult } from "../src/acp";
|
||||
import { ChevronDown, FolderOpen, Globe, Image, KeyRound, ScanLine, X } from "lucide-react";
|
||||
import { useQRScanner, type QRCodeData } from "../src/hooks";
|
||||
import { useState, useEffect, useLayoutEffect, useCallback, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Button } from './ui/button';
|
||||
import { StatusDot } from './ui/connection-status';
|
||||
import { ThemeToggle } from './ui/theme-toggle';
|
||||
import { Label } from './ui/label';
|
||||
import { InputGroup, InputGroupAddon, InputGroupInput } from './ui/input-group';
|
||||
import { ACPClient, DEFAULT_SETTINGS, DisconnectRequestedError } from '../src/acp';
|
||||
import type { ACPSettings, ConnectionState, BrowserToolParams, BrowserToolResult } from '../src/acp';
|
||||
import { ChevronDown, FolderOpen, Globe, Image, KeyRound, ScanLine, X } from 'lucide-react';
|
||||
import { useQRScanner, type QRCodeData } from '../src/hooks';
|
||||
|
||||
// Get token from the URL fragment so it is not sent in HTTP requests.
|
||||
function getTokenFromUrl(): string | undefined {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ""));
|
||||
return hashParams.get("token") || undefined;
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ''));
|
||||
return hashParams.get('token') || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
@@ -30,12 +26,12 @@ function getTokenFromUrl(): string | undefined {
|
||||
function inferProxyUrlFromPage(): string | undefined {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ""));
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ''));
|
||||
// Only infer if we have a fragment token (indicates user came from server-printed URL)
|
||||
if (!hashParams.has("token")) {
|
||||
if (!hashParams.has('token')) {
|
||||
return undefined;
|
||||
}
|
||||
const protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
||||
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return `${protocol}//${url.host}/ws`;
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -45,15 +41,15 @@ function inferProxyUrlFromPage(): string | undefined {
|
||||
function scrubTokenFromUrl(): void {
|
||||
try {
|
||||
const url = new URL(window.location.href);
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ""));
|
||||
if (!hashParams.has("token")) {
|
||||
const hashParams = new URLSearchParams(url.hash.replace(/^#/, ''));
|
||||
if (!hashParams.has('token')) {
|
||||
return;
|
||||
}
|
||||
|
||||
hashParams.delete("token");
|
||||
hashParams.delete('token');
|
||||
const nextHash = hashParams.toString();
|
||||
url.hash = nextHash ? `#${nextHash}` : "";
|
||||
window.history.replaceState(null, "", url.toString());
|
||||
url.hash = nextHash ? `#${nextHash}` : '';
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
@@ -102,11 +98,11 @@ export function ACPConnect({
|
||||
browserToolHandler,
|
||||
showTokenInput = false,
|
||||
inferFromUrl = false,
|
||||
placeholder = "Proxy server URL",
|
||||
placeholder = 'Proxy server URL',
|
||||
showScanButton = false,
|
||||
}: ACPConnectProps) {
|
||||
const [settings, setSettings] = useState<ACPSettings>(() => getInitialSettings(inferFromUrl));
|
||||
const [connectionState, setConnectionState] = useState<ConnectionState>("disconnected");
|
||||
const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isShaking, setIsShaking] = useState(false);
|
||||
const [client, setClient] = useState<ACPClient | null>(null);
|
||||
@@ -122,7 +118,7 @@ export function ACPConnect({
|
||||
// Mark for auto-connect (will be triggered by settings useEffect)
|
||||
pendingAutoConnectRef.current = true;
|
||||
// Update settings - this will trigger auto-connect via useEffect
|
||||
setSettings((prev) => ({
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
proxyUrl: data.url,
|
||||
token: data.token,
|
||||
@@ -163,9 +159,9 @@ export function ACPConnect({
|
||||
stopScanning(); // Close the scanner overlay after album scan
|
||||
}
|
||||
// Reset input to allow re-selecting the same file
|
||||
e.target.value = "";
|
||||
e.target.value = '';
|
||||
},
|
||||
[scanFromFile, stopScanning]
|
||||
[scanFromFile, stopScanning],
|
||||
);
|
||||
|
||||
// Open file picker
|
||||
@@ -203,7 +199,7 @@ export function ACPConnect({
|
||||
// Auto-connect after QR scan (when pendingAutoConnectRef is set)
|
||||
if (pendingAutoConnectRef.current) {
|
||||
pendingAutoConnectRef.current = false;
|
||||
client.connect().catch((e) => {
|
||||
client.connect().catch(e => {
|
||||
// Ignore disconnect requested - user cancelled intentionally
|
||||
if (e instanceof DisconnectRequestedError) {
|
||||
return;
|
||||
@@ -219,7 +215,7 @@ export function ACPConnect({
|
||||
|
||||
// Notify parent when client is ready and auto-collapse on connect
|
||||
useEffect(() => {
|
||||
const isConnected = connectionState === "connected";
|
||||
const isConnected = connectionState === 'connected';
|
||||
onClientReady?.(isConnected ? client : null);
|
||||
|
||||
// Auto-collapse when connected for the first time
|
||||
@@ -229,14 +225,14 @@ export function ACPConnect({
|
||||
}
|
||||
|
||||
// Reset auto-collapse flag when disconnected
|
||||
if (connectionState === "disconnected") {
|
||||
if (connectionState === 'disconnected') {
|
||||
hasAutoCollapsedRef.current = false;
|
||||
}
|
||||
}, [connectionState, client, onClientReady, onExpandedChange]);
|
||||
|
||||
const handleConnect = useCallback(async () => {
|
||||
// Prevent duplicate connect calls if already connecting or connected
|
||||
if (!client || connectionState === "connecting" || connectionState === "connected") {
|
||||
if (!client || connectionState === 'connecting' || connectionState === 'connected') {
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
@@ -263,7 +259,7 @@ export function ACPConnect({
|
||||
}, [client]);
|
||||
|
||||
const updateSetting = <K extends keyof ACPSettings>(key: K, value: ACPSettings[K]) => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
setSettings(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
// Clear error when starting to scan
|
||||
@@ -272,107 +268,93 @@ export function ACPConnect({
|
||||
startScanning();
|
||||
}, [startScanning]);
|
||||
|
||||
const isConnected = connectionState === "connected";
|
||||
const isConnecting = connectionState === "connecting";
|
||||
const isConnected = connectionState === 'connected';
|
||||
const isConnecting = connectionState === 'connecting';
|
||||
|
||||
const handleInputKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !isConnected && !isConnecting) {
|
||||
e.preventDefault();
|
||||
handleConnect();
|
||||
}
|
||||
}, [isConnected, isConnecting, handleConnect]);
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !isConnected && !isConnecting) {
|
||||
e.preventDefault();
|
||||
handleConnect();
|
||||
}
|
||||
},
|
||||
[isConnected, isConnecting, handleConnect],
|
||||
);
|
||||
|
||||
// Format URL for display
|
||||
const displayUrl = settings.proxyUrl.replace(/^wss?:\/\//, "").replace(/\/ws$/, "");
|
||||
const displayUrl = settings.proxyUrl.replace(/^wss?:\/\//, '').replace(/\/ws$/, '');
|
||||
|
||||
// Get status label
|
||||
const statusLabels: Record<ConnectionState, string> = {
|
||||
disconnected: "Disconnected",
|
||||
connecting: "Connecting...",
|
||||
connected: "Connected",
|
||||
error: "Error",
|
||||
disconnected: 'Disconnected',
|
||||
connecting: 'Connecting...',
|
||||
connected: 'Connected',
|
||||
error: 'Error',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-background/80 backdrop-blur-sm">
|
||||
<div className="max-w-md mx-auto border-b">
|
||||
{/* Status Bar - Always visible */}
|
||||
<button
|
||||
onClick={() => onExpandedChange(!expanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot state={connectionState} />
|
||||
<span className="text-sm font-medium">{statusLabels[connectionState]}</span>
|
||||
{isConnected && displayUrl && (
|
||||
<span className="text-xs text-muted-foreground">• {displayUrl}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<ThemeToggle />
|
||||
{/* Status Bar - Always visible */}
|
||||
<button
|
||||
onClick={() => onExpandedChange(!expanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusDot state={connectionState} />
|
||||
<span className="text-sm font-medium">{statusLabels[connectionState]}</span>
|
||||
{isConnected && displayUrl && <span className="text-xs text-muted-foreground">• {displayUrl}</span>}
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-muted-foreground transition-transform duration-200 ${
|
||||
expanded ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 text-muted-foreground transition-transform duration-200 ${
|
||||
expanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Expandable Settings Panel */}
|
||||
<div
|
||||
className="overflow-hidden transition-all duration-200 ease-out"
|
||||
style={{
|
||||
maxHeight: expanded ? maxHeight : 0,
|
||||
opacity: expanded ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef} className={`px-3 pb-3 pt-1 space-y-3 ${isShaking ? "animate-shake" : ""}`}>
|
||||
{/* Hidden file input for album scanning */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
{/* Expandable Settings Panel */}
|
||||
<div
|
||||
className="overflow-hidden transition-all duration-200 ease-out"
|
||||
style={{
|
||||
maxHeight: expanded ? maxHeight : 0,
|
||||
opacity: expanded ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div ref={contentRef} className={`px-3 pb-3 pt-1 space-y-3 ${isShaking ? 'animate-shake' : ''}`}>
|
||||
{/* Hidden file input for album scanning */}
|
||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={handleFileSelect} className="hidden" />
|
||||
|
||||
{/* QR Scanner View - Portal to body to escape backdrop-blur containing block */}
|
||||
{isScanning && createPortal(
|
||||
<div className="fixed inset-0 z-50 bg-black flex flex-col">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="flex-1 w-full object-cover"
|
||||
/>
|
||||
<Button
|
||||
onClick={stopScanning}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-4 right-4 h-10 w-10 p-0 bg-black/50 hover:bg-black/70 text-white rounded-full"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="absolute bottom-16 left-0 right-0 flex flex-col items-center gap-3">
|
||||
<Button
|
||||
onClick={handleSelectFromAlbum}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-9 px-4"
|
||||
>
|
||||
<Image className="h-4 w-4 mr-2" />
|
||||
Select from Album
|
||||
</Button>
|
||||
<span className="text-sm text-white/80">
|
||||
or point camera at QR code
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
{/* QR Scanner View - Portal to body to escape backdrop-blur containing block */}
|
||||
{isScanning &&
|
||||
createPortal(
|
||||
<div className="fixed inset-0 z-50 bg-black flex flex-col">
|
||||
<video ref={videoRef} className="flex-1 w-full object-cover" />
|
||||
<Button
|
||||
onClick={stopScanning}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute top-4 right-4 h-10 w-10 p-0 bg-black/50 hover:bg-black/70 text-white rounded-full"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
<div className="absolute bottom-16 left-0 right-0 flex flex-col items-center gap-3">
|
||||
<Button onClick={handleSelectFromAlbum} variant="secondary" size="sm" className="h-9 px-4">
|
||||
<Image className="h-4 w-4 mr-2" />
|
||||
Select from Album
|
||||
</Button>
|
||||
<span className="text-sm text-white/80">or point camera at QR code</span>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
||||
{/* Connection Settings - use invisible (not hidden) to preserve scrollHeight for animation */}
|
||||
<div className={`space-y-3 ${isScanning ? "invisible" : ""}`}>
|
||||
{/* Connection Settings - use invisible (not hidden) to preserve scrollHeight for animation */}
|
||||
<div className={`space-y-3 ${isScanning ? 'invisible' : ''}`}>
|
||||
{/* Server URL */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="proxy-url">Server</Label>
|
||||
@@ -396,7 +378,7 @@ export function ACPConnect({
|
||||
<InputGroupInput
|
||||
id="proxy-url"
|
||||
value={settings.proxyUrl}
|
||||
onChange={(e) => updateSetting("proxyUrl", e.target.value)}
|
||||
onChange={e => updateSetting('proxyUrl', e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={isConnected || isConnecting}
|
||||
@@ -411,7 +393,7 @@ export function ACPConnect({
|
||||
className="h-9 px-4"
|
||||
type="button"
|
||||
>
|
||||
{isConnecting ? "..." : "Connect"}
|
||||
{isConnecting ? '...' : 'Connect'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -440,8 +422,8 @@ export function ACPConnect({
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="auth-token"
|
||||
value={settings.token || ""}
|
||||
onChange={(e) => updateSetting("token", e.target.value || undefined)}
|
||||
value={settings.token || ''}
|
||||
onChange={e => updateSetting('token', e.target.value || undefined)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder="For remote access"
|
||||
disabled={isConnected || isConnecting}
|
||||
@@ -465,8 +447,8 @@ export function ACPConnect({
|
||||
</InputGroupAddon>
|
||||
<InputGroupInput
|
||||
id="working-dir"
|
||||
value={settings.cwd || ""}
|
||||
onChange={(e) => updateSetting("cwd", e.target.value || undefined)}
|
||||
value={settings.cwd || ''}
|
||||
onChange={e => updateSetting('cwd', e.target.value || undefined)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder="/path/to/project"
|
||||
disabled={isConnected || isConnecting}
|
||||
@@ -475,17 +457,13 @@ export function ACPConnect({
|
||||
/>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="text-xs text-destructive bg-destructive/10 px-2 py-1.5 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Message */}
|
||||
{error && <div className="text-xs text-destructive bg-destructive/10 px-2 py-1.5 rounded">{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user