Download src/bridge/bridgeUI.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 16.8 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/bridge/bridgeUI.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/bridge/bridgeUI.ts
-
curl -L -o bridgeUI.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/bridge/bridgeUI.ts
16.8 kB
| import chalk from 'chalk' | |
| import { toString as qrToString } from 'qrcode' | |
| import { | |
| BRIDGE_FAILED_INDICATOR, | |
| BRIDGE_READY_INDICATOR, | |
| BRIDGE_SPINNER_FRAMES, | |
| } from '../constants/figures.js' | |
| import { stringWidth } from '../ink/stringWidth.js' | |
| import { logForDebugging } from '../utils/debug.js' | |
| import { | |
| buildActiveFooterText, | |
| buildBridgeConnectUrl, | |
| buildBridgeSessionUrl, | |
| buildIdleFooterText, | |
| FAILED_FOOTER_TEXT, | |
| formatDuration, | |
| type StatusState, | |
| TOOL_DISPLAY_EXPIRY_MS, | |
| timestamp, | |
| truncatePrompt, | |
| wrapWithOsc8Link, | |
| } from './bridgeStatusUtil.js' | |
| import type { | |
| BridgeConfig, | |
| BridgeLogger, | |
| SessionActivity, | |
| SpawnMode, | |
| } from './types.js' | |
| const QR_OPTIONS = { | |
| type: 'utf8' as const, | |
| errorCorrectionLevel: 'L' as const, | |
| small: true, | |
| } | |
| /** Generate a QR code and return its lines. */ | |
| async function generateQr(url: string): Promise<string[]> { | |
| const qr = await qrToString(url, QR_OPTIONS) | |
| return qr.split('\n').filter((line: string) => line.length > 0) | |
| } | |
| export function createBridgeLogger(options: { | |
| verbose: boolean | |
| write?: (s: string) => void | |
| }): BridgeLogger { | |
| const write = options.write ?? ((s: string) => process.stdout.write(s)) | |
| const verbose = options.verbose | |
| // Track how many status lines are currently displayed at the bottom | |
| let statusLineCount = 0 | |
| // Status state machine | |
| let currentState: StatusState = 'idle' | |
| let currentStateText = 'Ready' | |
| let repoName = '' | |
| let branch = '' | |
| let debugLogPath = '' | |
| // Connect URL (built in printBanner with correct base for staging/prod) | |
| let connectUrl = '' | |
| let cachedIngressUrl = '' | |
| let cachedEnvironmentId = '' | |
| let activeSessionUrl: string | null = null | |
| // QR code lines for the current URL | |
| let qrLines: string[] = [] | |
| let qrVisible = false | |
| // Tool activity for the second status line | |
| let lastToolSummary: string | null = null | |
| let lastToolTime = 0 | |
| // Session count indicator (shown when multi-session mode is enabled) | |
| let sessionActive = 0 | |
| let sessionMax = 1 | |
| // Spawn mode shown in the session-count line + gates the `w` hint | |
| let spawnModeDisplay: 'same-dir' | 'worktree' | null = null | |
| let spawnMode: SpawnMode = 'single-session' | |
| // Per-session display info for the multi-session bullet list (keyed by compat sessionId) | |
| const sessionDisplayInfo = new Map< | |
| string, | |
| { title?: string; url: string; activity?: SessionActivity } | |
| >() | |
| // Connecting spinner state | |
| let connectingTimer: ReturnType<typeof setInterval> | null = null | |
| let connectingTick = 0 | |
| /** | |
| * Count how many visual terminal rows a string occupies, accounting for | |
| * line wrapping. Each `\n` is one row, and content wider than the terminal | |
| * wraps to additional rows. | |
| */ | |
| function countVisualLines(text: string): number { | |
| // eslint-disable-next-line custom-rules/prefer-use-terminal-size | |
| const cols = process.stdout.columns || 80 // non-React CLI context | |
| let count = 0 | |
| // Split on newlines to get logical lines | |
| for (const logical of text.split('\n')) { | |
| if (logical.length === 0) { | |
| // Empty segment between consecutive \n — counts as 1 row | |
| count++ | |
| continue | |
| } | |
| const width = stringWidth(logical) | |
| count += Math.max(1, Math.ceil(width / cols)) | |
| } | |
| // The trailing \n in "line\n" produces an empty last element — don't count it | |
| // because the cursor sits at the start of the next line, not a new visual row. | |
| if (text.endsWith('\n')) { | |
| count-- | |
| } | |
| return count | |
| } | |
| /** Write a status line and track its visual line count. */ | |
| function writeStatus(text: string): void { | |
| write(text) | |
| statusLineCount += countVisualLines(text) | |
| } | |
| /** Clear any currently displayed status lines. */ | |
| function clearStatusLines(): void { | |
| if (statusLineCount <= 0) return | |
| logForDebugging(`[bridge:ui] clearStatusLines count=${statusLineCount}`) | |
| // Move cursor up to the start of the status block, then erase everything below | |
| write(`\x1b[${statusLineCount}A`) // cursor up N lines | |
| write('\x1b[J') // erase from cursor to end of screen | |
| statusLineCount = 0 | |
| } | |
| /** Print a permanent log line, clearing status first and restoring after. */ | |
| function printLog(line: string): void { | |
| clearStatusLines() | |
| write(line) | |
| } | |
| /** Regenerate the QR code with the given URL. */ | |
| function regenerateQr(url: string): void { | |
| generateQr(url) | |
| .then(lines => { | |
| qrLines = lines | |
| renderStatusLine() | |
| }) | |
| .catch(e => { | |
| logForDebugging(`QR code generation failed: ${e}`, { level: 'error' }) | |
| }) | |
| } | |
| /** Render the connecting spinner line (shown before first updateIdleStatus). */ | |
| function renderConnectingLine(): void { | |
| clearStatusLines() | |
| const frame = | |
| BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]! | |
| let suffix = '' | |
| if (repoName) { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName) | |
| } | |
| if (branch) { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch) | |
| } | |
| writeStatus( | |
| `${chalk.yellow(frame)} ${chalk.yellow('Connecting')}${suffix}\n`, | |
| ) | |
| } | |
| /** Start the connecting spinner. Stopped by first updateIdleStatus(). */ | |
| function startConnecting(): void { | |
| stopConnecting() | |
| renderConnectingLine() | |
| connectingTimer = setInterval(() => { | |
| connectingTick++ | |
| renderConnectingLine() | |
| }, 150) | |
| } | |
| /** Stop the connecting spinner. */ | |
| function stopConnecting(): void { | |
| if (connectingTimer) { | |
| clearInterval(connectingTimer) | |
| connectingTimer = null | |
| } | |
| } | |
| /** Render and write the current status lines based on state. */ | |
| function renderStatusLine(): void { | |
| if (currentState === 'reconnecting' || currentState === 'failed') { | |
| // These states are handled separately (updateReconnectingStatus / | |
| // updateFailedStatus). Return before clearing so callers like toggleQr | |
| // and setSpawnModeDisplay don't blank the display during these states. | |
| return | |
| } | |
| clearStatusLines() | |
| const isIdle = currentState === 'idle' | |
| // QR code above the status line | |
| if (qrVisible) { | |
| for (const line of qrLines) { | |
| writeStatus(`${chalk.dim(line)}\n`) | |
| } | |
| } | |
| // Determine indicator and colors based on state | |
| const indicator = BRIDGE_READY_INDICATOR | |
| const indicatorColor = isIdle ? chalk.green : chalk.cyan | |
| const baseColor = isIdle ? chalk.green : chalk.cyan | |
| const stateText = baseColor(currentStateText) | |
| // Build the suffix with repo and branch | |
| let suffix = '' | |
| if (repoName) { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName) | |
| } | |
| // In worktree mode each session gets its own branch, so showing the | |
| // bridge's branch would be misleading. | |
| if (branch && spawnMode !== 'worktree') { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch) | |
| } | |
| if (process.env.USER_TYPE === 'ant' && debugLogPath) { | |
| writeStatus( | |
| `${chalk.yellow('[ANT-ONLY] Logs:')} ${chalk.dim(debugLogPath)}\n`, | |
| ) | |
| } | |
| writeStatus(`${indicatorColor(indicator)} ${stateText}${suffix}\n`) | |
| // Session count and per-session list (multi-session mode only) | |
| if (sessionMax > 1) { | |
| const modeHint = | |
| spawnMode === 'worktree' | |
| ? 'New sessions will be created in an isolated worktree' | |
| : 'New sessions will be created in the current directory' | |
| writeStatus( | |
| ` ${chalk.dim(`Capacity: ${sessionActive}/${sessionMax} \u00b7 ${modeHint}`)}\n`, | |
| ) | |
| for (const [, info] of sessionDisplayInfo) { | |
| const titleText = info.title | |
| ? truncatePrompt(info.title, 35) | |
| : chalk.dim('Attached') | |
| const titleLinked = wrapWithOsc8Link(titleText, info.url) | |
| const act = info.activity | |
| const showAct = act && act.type !== 'result' && act.type !== 'error' | |
| const actText = showAct | |
| ? chalk.dim(` ${truncatePrompt(act.summary, 40)}`) | |
| : '' | |
| writeStatus(` ${titleLinked}${actText} | |
| `) | |
| } | |
| } | |
| // Mode line for spawn modes with a single slot (or true single-session mode) | |
| if (sessionMax === 1) { | |
| const modeText = | |
| spawnMode === 'single-session' | |
| ? 'Single session \u00b7 exits when complete' | |
| : spawnMode === 'worktree' | |
| ? `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in an isolated worktree` | |
| : `Capacity: ${sessionActive}/1 \u00b7 New sessions will be created in the current directory` | |
| writeStatus(` ${chalk.dim(modeText)}\n`) | |
| } | |
| // Tool activity line for single-session mode | |
| if ( | |
| sessionMax === 1 && | |
| !isIdle && | |
| lastToolSummary && | |
| Date.now() - lastToolTime < TOOL_DISPLAY_EXPIRY_MS | |
| ) { | |
| writeStatus(` ${chalk.dim(truncatePrompt(lastToolSummary, 60))}\n`) | |
| } | |
| // Blank line separator before footer | |
| const url = activeSessionUrl ?? connectUrl | |
| if (url) { | |
| writeStatus('\n') | |
| const footerText = isIdle | |
| ? buildIdleFooterText(url) | |
| : buildActiveFooterText(url) | |
| const qrHint = qrVisible | |
| ? chalk.dim.italic('space to hide QR code') | |
| : chalk.dim.italic('space to show QR code') | |
| const toggleHint = spawnModeDisplay | |
| ? chalk.dim.italic(' \u00b7 w to toggle spawn mode') | |
| : '' | |
| writeStatus(`${chalk.dim(footerText)}\n`) | |
| writeStatus(`${qrHint}${toggleHint}\n`) | |
| } | |
| } | |
| return { | |
| printBanner(config: BridgeConfig, environmentId: string): void { | |
| cachedIngressUrl = config.sessionIngressUrl | |
| cachedEnvironmentId = environmentId | |
| connectUrl = buildBridgeConnectUrl(environmentId, cachedIngressUrl) | |
| regenerateQr(connectUrl) | |
| if (verbose) { | |
| write(chalk.dim(`Remote Control`) + ` v${MACRO.VERSION}\n`) | |
| } | |
| if (verbose) { | |
| if (config.spawnMode !== 'single-session') { | |
| write(chalk.dim(`Spawn mode: `) + `${config.spawnMode}\n`) | |
| write( | |
| chalk.dim(`Max concurrent sessions: `) + `${config.maxSessions}\n`, | |
| ) | |
| } | |
| write(chalk.dim(`Environment ID: `) + `${environmentId}\n`) | |
| } | |
| if (config.sandbox) { | |
| write(chalk.dim(`Sandbox: `) + `${chalk.green('Enabled')}\n`) | |
| } | |
| write('\n') | |
| // Start connecting spinner — first updateIdleStatus() will stop it | |
| startConnecting() | |
| }, | |
| logSessionStart(sessionId: string, prompt: string): void { | |
| if (verbose) { | |
| const short = truncatePrompt(prompt, 80) | |
| printLog( | |
| chalk.dim(`[${timestamp()}]`) + | |
| ` Session started: ${chalk.white(`"${short}"`)} (${chalk.dim(sessionId)})\n`, | |
| ) | |
| } | |
| }, | |
| logSessionComplete(sessionId: string, durationMs: number): void { | |
| printLog( | |
| chalk.dim(`[${timestamp()}]`) + | |
| ` Session ${chalk.green('completed')} (${formatDuration(durationMs)}) ${chalk.dim(sessionId)}\n`, | |
| ) | |
| }, | |
| logSessionFailed(sessionId: string, error: string): void { | |
| printLog( | |
| chalk.dim(`[${timestamp()}]`) + | |
| ` Session ${chalk.red('failed')}: ${error} ${chalk.dim(sessionId)}\n`, | |
| ) | |
| }, | |
| logStatus(message: string): void { | |
| printLog(chalk.dim(`[${timestamp()}]`) + ` ${message}\n`) | |
| }, | |
| logVerbose(message: string): void { | |
| if (verbose) { | |
| printLog(chalk.dim(`[${timestamp()}] ${message}`) + '\n') | |
| } | |
| }, | |
| logError(message: string): void { | |
| printLog(chalk.red(`[${timestamp()}] Error: ${message}`) + '\n') | |
| }, | |
| logReconnected(disconnectedMs: number): void { | |
| printLog( | |
| chalk.dim(`[${timestamp()}]`) + | |
| ` ${chalk.green('Reconnected')} after ${formatDuration(disconnectedMs)}\n`, | |
| ) | |
| }, | |
| setRepoInfo(repo: string, branchName: string): void { | |
| repoName = repo | |
| branch = branchName | |
| }, | |
| setDebugLogPath(path: string): void { | |
| debugLogPath = path | |
| }, | |
| updateIdleStatus(): void { | |
| stopConnecting() | |
| currentState = 'idle' | |
| currentStateText = 'Ready' | |
| lastToolSummary = null | |
| lastToolTime = 0 | |
| activeSessionUrl = null | |
| regenerateQr(connectUrl) | |
| renderStatusLine() | |
| }, | |
| setAttached(sessionId: string): void { | |
| stopConnecting() | |
| currentState = 'attached' | |
| currentStateText = 'Connected' | |
| lastToolSummary = null | |
| lastToolTime = 0 | |
| // Multi-session: keep footer/QR on the environment connect URL so users | |
| // can spawn more sessions. Per-session links are in the bullet list. | |
| if (sessionMax <= 1) { | |
| activeSessionUrl = buildBridgeSessionUrl( | |
| sessionId, | |
| cachedEnvironmentId, | |
| cachedIngressUrl, | |
| ) | |
| regenerateQr(activeSessionUrl) | |
| } | |
| renderStatusLine() | |
| }, | |
| updateReconnectingStatus(delayStr: string, elapsedStr: string): void { | |
| stopConnecting() | |
| clearStatusLines() | |
| currentState = 'reconnecting' | |
| // QR code above the status line | |
| if (qrVisible) { | |
| for (const line of qrLines) { | |
| writeStatus(`${chalk.dim(line)}\n`) | |
| } | |
| } | |
| const frame = | |
| BRIDGE_SPINNER_FRAMES[connectingTick % BRIDGE_SPINNER_FRAMES.length]! | |
| connectingTick++ | |
| writeStatus( | |
| `${chalk.yellow(frame)} ${chalk.yellow('Reconnecting')} ${chalk.dim('\u00b7')} ${chalk.dim(`retrying in ${delayStr}`)} ${chalk.dim('\u00b7')} ${chalk.dim(`disconnected ${elapsedStr}`)}\n`, | |
| ) | |
| }, | |
| updateFailedStatus(error: string): void { | |
| stopConnecting() | |
| clearStatusLines() | |
| currentState = 'failed' | |
| let suffix = '' | |
| if (repoName) { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(repoName) | |
| } | |
| if (branch) { | |
| suffix += chalk.dim(' \u00b7 ') + chalk.dim(branch) | |
| } | |
| writeStatus( | |
| `${chalk.red(BRIDGE_FAILED_INDICATOR)} ${chalk.red('Remote Control Failed')}${suffix}\n`, | |
| ) | |
| writeStatus(`${chalk.dim(FAILED_FOOTER_TEXT)}\n`) | |
| if (error) { | |
| writeStatus(`${chalk.red(error)}\n`) | |
| } | |
| }, | |
| updateSessionStatus( | |
| _sessionId: string, | |
| _elapsed: string, | |
| activity: SessionActivity, | |
| _trail: string[], | |
| ): void { | |
| // Cache tool activity for the second status line | |
| if (activity.type === 'tool_start') { | |
| lastToolSummary = activity.summary | |
| lastToolTime = Date.now() | |
| } | |
| renderStatusLine() | |
| }, | |
| clearStatus(): void { | |
| stopConnecting() | |
| clearStatusLines() | |
| }, | |
| toggleQr(): void { | |
| qrVisible = !qrVisible | |
| renderStatusLine() | |
| }, | |
| updateSessionCount(active: number, max: number, mode: SpawnMode): void { | |
| if (sessionActive === active && sessionMax === max && spawnMode === mode) | |
| return | |
| sessionActive = active | |
| sessionMax = max | |
| spawnMode = mode | |
| // Don't re-render here — the status ticker calls renderStatusLine | |
| // on its own cadence, and the next tick will pick up the new values. | |
| }, | |
| setSpawnModeDisplay(mode: 'same-dir' | 'worktree' | null): void { | |
| if (spawnModeDisplay === mode) return | |
| spawnModeDisplay = mode | |
| // Also sync the #21118-added spawnMode so the next render shows correct | |
| // mode hint + branch visibility. Don't render here — matches | |
| // updateSessionCount: called before printBanner (initial setup) and | |
| // again from the `w` handler (which follows with refreshDisplay). | |
| if (mode) spawnMode = mode | |
| }, | |
| addSession(sessionId: string, url: string): void { | |
| sessionDisplayInfo.set(sessionId, { url }) | |
| }, | |
| updateSessionActivity(sessionId: string, activity: SessionActivity): void { | |
| const info = sessionDisplayInfo.get(sessionId) | |
| if (!info) return | |
| info.activity = activity | |
| }, | |
| setSessionTitle(sessionId: string, title: string): void { | |
| const info = sessionDisplayInfo.get(sessionId) | |
| if (!info) return | |
| info.title = title | |
| // Guard against reconnecting/failed — renderStatusLine clears then returns | |
| // early for those states, which would erase the spinner/error. | |
| if (currentState === 'reconnecting' || currentState === 'failed') return | |
| if (sessionMax === 1) { | |
| // Single-session: show title in the main status line too. | |
| currentState = 'titled' | |
| currentStateText = truncatePrompt(title, 40) | |
| } | |
| renderStatusLine() | |
| }, | |
| removeSession(sessionId: string): void { | |
| sessionDisplayInfo.delete(sessionId) | |
| }, | |
| refreshDisplay(): void { | |
| // Skip during reconnecting/failed — renderStatusLine clears then returns | |
| // early for those states, which would erase the spinner/error. | |
| if (currentState === 'reconnecting' || currentState === 'failed') return | |
| renderStatusLine() | |
| }, | |
| } | |
| } | |