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: 'terminal', 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 ( {indicator} {statusLabel} {contextSuffix} {error && {error}} {verbose && environmentId && Environment: {environmentId}} {verbose && sessionId && Session: {sessionId}} {showQR && qrLines.length > 0 && ( {qrLines.map((line, i) => ( {line} ))} )} {footerText && {footerText}} d to disconnect · space for QR code · Enter/Esc to close ); }