bridgeStatusUtil.ts
1 import { 2 getClaudeAiBaseUrl, 3 getRemoteSessionUrl, 4 } from '../constants/product.js' 5 import { stringWidth } from '../ink/stringWidth.js' 6 import { formatDuration, truncateToWidth } from '../utils/format.js' 7 import { getGraphemeSegmenter } from '../utils/intl.js' 8 9 /** Bridge status state machine states. */ 10 export type StatusState = 11 | 'idle' 12 | 'attached' 13 | 'titled' 14 | 'reconnecting' 15 | 'failed' 16 17 /** How long a tool activity line stays visible after last tool_start (ms). */ 18 export const TOOL_DISPLAY_EXPIRY_MS = 30_000 19 20 /** Interval for the shimmer animation tick (ms). */ 21 export const SHIMMER_INTERVAL_MS = 150 22 23 export function timestamp(): string { 24 const now = new Date() 25 const h = String(now.getHours()).padStart(2, '0') 26 const m = String(now.getMinutes()).padStart(2, '0') 27 const s = String(now.getSeconds()).padStart(2, '0') 28 return `${h}:${m}:${s}` 29 } 30 31 export { formatDuration, truncateToWidth as truncatePrompt } 32 33 /** Abbreviate a tool activity summary for the trail display. */ 34 export function abbreviateActivity(summary: string): string { 35 return truncateToWidth(summary, 30) 36 } 37 38 /** Build the connect URL shown when the bridge is idle. */ 39 export function buildBridgeConnectUrl( 40 environmentId: string, 41 ingressUrl?: string, 42 ): string { 43 const baseUrl = getClaudeAiBaseUrl(undefined, ingressUrl) 44 return `${baseUrl}/code?bridge=${environmentId}` 45 } 46 47 /** 48 * Build the session URL shown when a session is attached. Delegates to 49 * getRemoteSessionUrl for the cse_→session_ prefix translation, then appends 50 * the v1-specific ?bridge={environmentId} query. 51 */ 52 export function buildBridgeSessionUrl( 53 sessionId: string, 54 environmentId: string, 55 ingressUrl?: string, 56 ): string { 57 return `${getRemoteSessionUrl(sessionId, ingressUrl)}?bridge=${environmentId}` 58 } 59 60 /** Compute the glimmer index for a reverse-sweep shimmer animation. */ 61 export function computeGlimmerIndex( 62 tick: number, 63 messageWidth: number, 64 ): number { 65 const cycleLength = messageWidth + 20 66 return messageWidth + 10 - (tick % cycleLength) 67 } 68 69 /** 70 * Split text into three segments by visual column position for shimmer rendering. 71 * 72 * Uses grapheme segmentation and `stringWidth` so the split is correct for 73 * multi-byte characters, emoji, and CJK glyphs. 74 * 75 * Returns `{ before, shimmer, after }` strings. Both renderers (chalk in 76 * bridgeUI.ts and React/Ink in bridge.tsx) apply their own coloring to 77 * these segments. 78 */ 79 export function computeShimmerSegments( 80 text: string, 81 glimmerIndex: number, 82 ): { before: string; shimmer: string; after: string } { 83 const messageWidth = stringWidth(text) 84 const shimmerStart = glimmerIndex - 1 85 const shimmerEnd = glimmerIndex + 1 86 87 // When shimmer is offscreen, return all text as "before" 88 if (shimmerStart >= messageWidth || shimmerEnd < 0) { 89 return { before: text, shimmer: '', after: '' } 90 } 91 92 // Split into at most 3 segments by visual column position 93 const clampedStart = Math.max(0, shimmerStart) 94 let colPos = 0 95 let before = '' 96 let shimmer = '' 97 let after = '' 98 for (const { segment } of getGraphemeSegmenter().segment(text)) { 99 const segWidth = stringWidth(segment) 100 if (colPos + segWidth <= clampedStart) { 101 before += segment 102 } else if (colPos > shimmerEnd) { 103 after += segment 104 } else { 105 shimmer += segment 106 } 107 colPos += segWidth 108 } 109 110 return { before, shimmer, after } 111 } 112 113 /** Computed bridge status label and color from connection state. */ 114 export type BridgeStatusInfo = { 115 label: 116 | 'Remote Control failed' 117 | 'Remote Control reconnecting' 118 | 'Remote Control active' 119 | 'Remote Control connecting\u2026' 120 color: 'error' | 'warning' | 'success' 121 } 122 123 /** Derive a status label and color from the bridge connection state. */ 124 export function getBridgeStatus({ 125 error, 126 connected, 127 sessionActive, 128 reconnecting, 129 }: { 130 error: string | undefined 131 connected: boolean 132 sessionActive: boolean 133 reconnecting: boolean 134 }): BridgeStatusInfo { 135 if (error) return { label: 'Remote Control failed', color: 'error' } 136 if (reconnecting) 137 return { label: 'Remote Control reconnecting', color: 'warning' } 138 if (sessionActive || connected) 139 return { label: 'Remote Control active', color: 'success' } 140 return { label: 'Remote Control connecting\u2026', color: 'warning' } 141 } 142 143 /** Footer text shown when bridge is idle (Ready state). */ 144 export function buildIdleFooterText(url: string): string { 145 return `Code everywhere with the Claude app or ${url}` 146 } 147 148 /** Footer text shown when a session is active (Connected state). */ 149 export function buildActiveFooterText(url: string): string { 150 return `Continue coding in the Claude app or ${url}` 151 } 152 153 /** Footer text shown when the bridge has failed. */ 154 export const FAILED_FOOTER_TEXT = 'Something went wrong, please try again' 155 156 /** 157 * Wrap text in an OSC 8 terminal hyperlink. Zero visual width for layout purposes. 158 * strip-ansi (used by stringWidth) correctly strips these sequences, so 159 * countVisualLines in bridgeUI.ts remains accurate. 160 */ 161 export function wrapWithOsc8Link(text: string, url: string): string { 162 return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07` 163 }