mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import { basename } from 'path'
|
|
import { toString as qrToString } from 'qrcode'
|
|
import * as React from 'react'
|
|
import { useEffect, useState } from 'react'
|
|
import { getOriginalCwd } from '../bootstrap/state.js'
|
|
import {
|
|
buildActiveFooterText,
|
|
buildIdleFooterText,
|
|
FAILED_FOOTER_TEXT,
|
|
getBridgeStatus,
|
|
} from '../bridge/bridgeStatusUtil.js'
|
|
import {
|
|
BRIDGE_FAILED_INDICATOR,
|
|
BRIDGE_READY_INDICATOR,
|
|
} from '../constants/figures.js'
|
|
import { useRegisterOverlay } from '../context/overlayContext.js'
|
|
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw 'd' key for disconnect, not a configurable keybinding action
|
|
import { Box, Text, useInput } from '@anthropic/ink'
|
|
import { useKeybindings } from '../keybindings/useKeybinding.js'
|
|
import { useAppState, useSetAppState } from '../state/AppState.js'
|
|
import { saveGlobalConfig } from '../utils/config.js'
|
|
import { getBranch } from '../utils/git.js'
|
|
import { Dialog } from '@anthropic/ink'
|
|
|
|
type Props = {
|
|
onDone: () => void
|
|
}
|
|
|
|
export function BridgeDialog({ onDone }: Props): React.ReactNode {
|
|
useRegisterOverlay('bridge-dialog')
|
|
|
|
const connected = useAppState(s => s.replBridgeConnected)
|
|
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
|
const reconnecting = useAppState(s => s.replBridgeReconnecting)
|
|
const connectUrl = useAppState(s => s.replBridgeConnectUrl)
|
|
const sessionUrl = useAppState(s => s.replBridgeSessionUrl)
|
|
const error = useAppState(s => s.replBridgeError)
|
|
const explicit = useAppState(s => s.replBridgeExplicit)
|
|
const environmentId = useAppState(s => s.replBridgeEnvironmentId)
|
|
const sessionId = useAppState(s => s.replBridgeSessionId)
|
|
const verbose = useAppState(s => s.verbose)
|
|
const setAppState = useSetAppState()
|
|
|
|
const [showQR, setShowQR] = useState(false)
|
|
const [qrText, setQrText] = useState('')
|
|
const [branchName, setBranchName] = useState('')
|
|
|
|
const repoName = basename(getOriginalCwd())
|
|
|
|
// Fetch branch name on mount
|
|
useEffect(() => {
|
|
getBranch()
|
|
.then(setBranchName)
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
// The URL to display/QR: session URL when connected, connect URL when ready
|
|
const displayUrl = sessionActive ? sessionUrl : connectUrl
|
|
|
|
// Generate QR code when URL changes or QR is toggled on
|
|
useEffect(() => {
|
|
if (!showQR || !displayUrl) {
|
|
setQrText('')
|
|
return
|
|
}
|
|
qrToString(displayUrl, {
|
|
type: 'utf8',
|
|
errorCorrectionLevel: 'L',
|
|
small: true,
|
|
})
|
|
.then(setQrText)
|
|
.catch(() => setQrText(''))
|
|
}, [showQR, displayUrl])
|
|
|
|
useKeybindings(
|
|
{
|
|
'confirm:yes': onDone,
|
|
'confirm:toggle': () => {
|
|
setShowQR(prev => !prev)
|
|
},
|
|
},
|
|
{ context: 'Confirmation' },
|
|
)
|
|
|
|
useInput(input => {
|
|
if (input === 'd') {
|
|
// Persist opt-out only for CLI-flag/command-activated bridge.
|
|
// Config-driven and GB-auto-connect users get session-only disconnect
|
|
// — writing false would silently undo a Settings choice or opt a
|
|
// GB-rollout user out permanently.
|
|
if (explicit) {
|
|
saveGlobalConfig(current => {
|
|
if (current.remoteControlAtStartup === false) return current
|
|
return { ...current, remoteControlAtStartup: false }
|
|
})
|
|
}
|
|
setAppState(prev => {
|
|
if (!prev.replBridgeEnabled) return prev
|
|
return { ...prev, replBridgeEnabled: false }
|
|
})
|
|
onDone()
|
|
}
|
|
})
|
|
|
|
const { label: statusLabel, color: statusColor } = getBridgeStatus({
|
|
error,
|
|
connected,
|
|
sessionActive,
|
|
reconnecting,
|
|
})
|
|
const indicator = error ? BRIDGE_FAILED_INDICATOR : BRIDGE_READY_INDICATOR
|
|
const qrLines = qrText ? qrText.split('\n').filter(l => l.length > 0) : []
|
|
|
|
// Build suffix with repo and branch (matches standalone bridge format)
|
|
const contextParts: string[] = []
|
|
if (repoName) contextParts.push(repoName)
|
|
if (branchName) contextParts.push(branchName)
|
|
const contextSuffix =
|
|
contextParts.length > 0 ? ' \u00b7 ' + contextParts.join(' \u00b7 ') : ''
|
|
|
|
// Footer text matches standalone bridge
|
|
const footerText = error
|
|
? FAILED_FOOTER_TEXT
|
|
: displayUrl
|
|
? sessionActive
|
|
? buildActiveFooterText(displayUrl)
|
|
: buildIdleFooterText(displayUrl)
|
|
: undefined
|
|
|
|
return (
|
|
<Dialog title="Remote Control" onCancel={onDone} hideInputGuide>
|
|
<Box flexDirection="column" gap={1}>
|
|
<Box flexDirection="column">
|
|
<Text>
|
|
<Text color={statusColor}>
|
|
{indicator} {statusLabel}
|
|
</Text>
|
|
<Text dimColor>{contextSuffix}</Text>
|
|
</Text>
|
|
{error && <Text color="error">{error}</Text>}
|
|
{verbose && environmentId && (
|
|
<Text dimColor>Environment: {environmentId}</Text>
|
|
)}
|
|
{verbose && sessionId && <Text dimColor>Session: {sessionId}</Text>}
|
|
</Box>
|
|
{showQR && qrLines.length > 0 && (
|
|
<Box flexDirection="column">
|
|
{qrLines.map((line, i) => (
|
|
<Text key={i}>{line}</Text>
|
|
))}
|
|
</Box>
|
|
)}
|
|
{footerText && <Text dimColor>{footerText}</Text>}
|
|
<Text dimColor>
|
|
d to disconnect · space for QR code · Enter/Esc to close
|
|
</Text>
|
|
</Box>
|
|
</Dialog>
|
|
)
|
|
}
|