2026-03-31 19:22:47 +08:00
import { c as _c } from "react/compiler-runtime" ;
// biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
import { Box , Text } from '../ink.js' ;
import * as React from 'react' ;
import { useEffect , useMemo , useRef , useState } from 'react' ;
import { computeGlimmerIndex , computeShimmerSegments , SHIMMER_INTERVAL_MS } from '../bridge/bridgeStatusUtil.js' ;
import { feature } from 'bun:bundle' ;
import { getKairosActive , getUserMsgOptIn } from '../bootstrap/state.js' ;
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' ;
import { isEnvTruthy } from '../utils/envUtils.js' ;
import { count } from '../utils/array.js' ;
import sample from 'lodash-es/sample.js' ;
import { formatDuration , formatNumber , formatSecondsShort } from '../utils/format.js' ;
import type { Theme } from 'src/utils/theme.js' ;
import { activityManager } from '../utils/activityManager.js' ;
import { getSpinnerVerbs } from '../constants/spinnerVerbs.js' ;
import { MessageResponse } from './MessageResponse.js' ;
import { TaskListV2 } from './TaskListV2.js' ;
import { useTasksV2 } from '../hooks/useTasksV2.js' ;
import type { Task } from '../utils/tasks.js' ;
import { useAppState } from '../state/AppState.js' ;
import { useTerminalSize } from '../hooks/useTerminalSize.js' ;
import { stringWidth } from '../ink/stringWidth.js' ;
import { getDefaultCharacters , type SpinnerMode } from './Spinner/index.js' ;
import { SpinnerAnimationRow } from './Spinner/SpinnerAnimationRow.js' ;
import { useSettings } from '../hooks/useSettings.js' ;
import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js' ;
import { isBackgroundTask } from '../tasks/types.js' ;
import { getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js' ;
import { getEffortSuffix } from '../utils/effort.js' ;
import { getMainLoopModel } from '../utils/model/model.js' ;
import { getViewedTeammateTask } from '../state/selectors.js' ;
import { TEARDROP_ASTERISK } from '../constants/figures.js' ;
import figures from 'figures' ;
import { getCurrentTurnTokenBudget , getTurnOutputTokens } from '../bootstrap/state.js' ;
import { TeammateSpinnerTree } from './Spinner/TeammateSpinnerTree.js' ;
import { useAnimationFrame } from '../ink.js' ;
import { getGlobalConfig } from '../utils/config.js' ;
export type { SpinnerMode } from './Spinner/index.js' ;
const DEFAULT_CHARACTERS = getDefaultCharacters ( ) ;
const SPINNER_FRAMES = [ . . . DEFAULT_CHARACTERS , . . . [ . . . DEFAULT_CHARACTERS ] . reverse ( ) ] ;
type Props = {
mode : SpinnerMode ;
loadingStartTimeRef : React.RefObject < number > ;
totalPausedMsRef : React.RefObject < number > ;
pauseStartTimeRef : React.RefObject < number | null > ;
spinnerTip? : string ;
responseLengthRef : React.RefObject < number > ;
2026-03-31 21:46:46 +08:00
apiMetricsRef? : React.RefObject < Array < { ttftMs : number ; firstTokenTime : number ; lastTokenTime : number ; responseLengthBaseline : number ; endResponseLength : number } > > ;
2026-03-31 19:22:47 +08:00
overrideColor? : keyof Theme | null ;
overrideShimmerColor? : keyof Theme | null ;
overrideMessage? : string | null ;
spinnerSuffix? : string | null ;
verbose : boolean ;
hasActiveTools? : boolean ;
/** Leader's turn has completed (no active query). Used to suppress stall-red spinner when only teammates are running. */
leaderIsIdle? : boolean ;
} ;
// Thin wrapper: branches on isBriefOnly so the two variants have independent
// hook call chains. Without this split, toggling /brief mid-render would
// violate Rules of Hooks (the inner variant calls ~10 more hooks).
export function SpinnerWithVerb ( props : Props ) : React . ReactNode {
const isBriefOnly = useAppState ( s = > s . isBriefOnly ) ;
// REPL overrides isBriefOnly→false when viewing a teammate transcript
// (see isBriefOnly={viewedTeammateTask ? false : isBriefOnly}). That
// prop isn't threaded here, so replicate the gate from the store —
// teammate view needs the real spinner (which shows teammate status).
const viewingAgentTaskId = useAppState ( s_0 = > s_0 . viewingAgentTaskId ) ;
// Hoisted to mount-time — this component re-renders at animation framerate.
const briefEnvEnabled = feature ( 'KAIROS' ) || feature ( 'KAIROS_BRIEF' ) ?
// biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
useMemo ( ( ) = > isEnvTruthy ( process . env . CLAUDE_CODE_BRIEF ) , [ ] ) : false ;
// Runtime gate mirrors isBriefEnabled() but inlined — importing from
// BriefTool.ts would leak tool-name strings into external builds. Single
// spinner instance → hooks stay unconditional (two subs, negligible).
if ( ( feature ( 'KAIROS' ) || feature ( 'KAIROS_BRIEF' ) ) && ( getKairosActive ( ) || getUserMsgOptIn ( ) && ( briefEnvEnabled || getFeatureValue_CACHED_MAY_BE_STALE ( 'tengu_kairos_brief' , false ) ) ) && isBriefOnly && ! viewingAgentTaskId ) {
return < BriefSpinner mode = { props . mode } overrideMessage = { props . overrideMessage } / > ;
}
return < SpinnerWithVerbInner { ...props } / > ;
}
function SpinnerWithVerbInner ( {
mode ,
loadingStartTimeRef ,
totalPausedMsRef ,
pauseStartTimeRef ,
spinnerTip ,
responseLengthRef ,
overrideColor ,
overrideShimmerColor ,
overrideMessage ,
spinnerSuffix ,
verbose ,
hasActiveTools = false ,
leaderIsIdle = false
} : Props ) : React . ReactNode {
const settings = useSettings ( ) ;
const reducedMotion = settings . prefersReducedMotion ? ? false ;
// NOTE: useAnimationFrame(50) lives in SpinnerAnimationRow, not here.
// This component only re-renders when props or app state change —
// it is no longer on the 50ms clock. All `time`-derived values
// (frame, glimmer, stalled intensity, token counter, thinking shimmer,
// elapsed-time timer) are computed inside the child.
const tasks = useAppState ( s = > s . tasks ) ;
const viewingAgentTaskId = useAppState ( s_0 = > s_0 . viewingAgentTaskId ) ;
const expandedView = useAppState ( s_1 = > s_1 . expandedView ) ;
const showExpandedTodos = expandedView === 'tasks' ;
const showSpinnerTree = expandedView === 'teammates' ;
const selectedIPAgentIndex = useAppState ( s_2 = > s_2 . selectedIPAgentIndex ) ;
const viewSelectionMode = useAppState ( s_3 = > s_3 . viewSelectionMode ) ;
// Get foregrounded teammate (if viewing a teammate's transcript)
const foregroundedTeammate = viewingAgentTaskId ? getViewedTeammateTask ( {
viewingAgentTaskId ,
tasks
} ) : undefined ;
const {
columns
} = useTerminalSize ( ) ;
const tasksV2 = useTasksV2 ( ) ;
// Track thinking status: 'thinking' | number (duration in ms) | null
// Shows each state for minimum 2s to avoid UI jank
const [ thinkingStatus , setThinkingStatus ] = useState < 'thinking' | number | null > ( null ) ;
const thinkingStartRef = useRef < number | null > ( null ) ;
useEffect ( ( ) = > {
let showDurationTimer : ReturnType < typeof setTimeout > | null = null ;
let clearStatusTimer : ReturnType < typeof setTimeout > | null = null ;
if ( mode === 'thinking' ) {
// Started thinking
if ( thinkingStartRef . current === null ) {
thinkingStartRef . current = Date . now ( ) ;
setThinkingStatus ( 'thinking' ) ;
}
} else if ( thinkingStartRef . current !== null ) {
// Stopped thinking - calculate duration and ensure 2s minimum display
const duration = Date . now ( ) - thinkingStartRef . current ;
const elapsed = Date . now ( ) - thinkingStartRef . current ;
const remainingThinkingTime = Math . max ( 0 , 2000 - elapsed ) ;
thinkingStartRef . current = null ;
// Show "thinking..." for remaining time if < 2s elapsed, then show duration
const showDuration = ( ) : void = > {
setThinkingStatus ( duration ) ;
// Clear after 2s
clearStatusTimer = setTimeout ( setThinkingStatus , 2000 , null ) ;
} ;
if ( remainingThinkingTime > 0 ) {
showDurationTimer = setTimeout ( showDuration , remainingThinkingTime ) ;
} else {
showDuration ( ) ;
}
}
return ( ) = > {
if ( showDurationTimer ) clearTimeout ( showDurationTimer ) ;
if ( clearStatusTimer ) clearTimeout ( clearStatusTimer ) ;
} ;
} , [ mode ] ) ;
// Find the current in-progress task and next pending task
const currentTodo = tasksV2 ? . find ( task = > task . status !== 'pending' && task . status !== 'completed' ) ;
const nextTask = findNextPendingTask ( tasksV2 ) ;
// Use useState with initializer to pick a random verb once on mount
const [ randomVerb ] = useState ( ( ) = > sample ( getSpinnerVerbs ( ) ) ) ;
// Leader's own verb (always the leader's, regardless of who is foregrounded)
const leaderVerb = overrideMessage ? ? currentTodo ? . activeForm ? ? currentTodo ? . subject ? ? randomVerb ;
const effectiveVerb = foregroundedTeammate && ! foregroundedTeammate . isIdle ? foregroundedTeammate . spinnerVerb ? ? randomVerb : leaderVerb ;
const message = effectiveVerb + '…' ;
// Track CLI activity when spinner is active
useEffect ( ( ) = > {
const operationId = 'spinner-' + mode ;
activityManager . startCLIActivity ( operationId ) ;
return ( ) = > {
activityManager . endCLIActivity ( operationId ) ;
} ;
} , [ mode ] ) ;
const effortValue = useAppState ( s_4 = > s_4 . effortValue ) ;
const effortSuffix = getEffortSuffix ( getMainLoopModel ( ) , effortValue ) ;
// Check if any running in-process teammates exist (needed for both modes)
const runningTeammates = getAllInProcessTeammateTasks ( tasks ) . filter ( t = > t . status === 'running' ) ;
const hasRunningTeammates = runningTeammates . length > 0 ;
const allIdle = hasRunningTeammates && runningTeammates . every ( t_0 = > t_0 . isIdle ) ;
// Gather aggregate token stats from all running swarm teammates
// In spinner-tree mode, skip aggregation (teammates have their own lines in the tree)
let teammateTokens = 0 ;
if ( ! showSpinnerTree ) {
for ( const task_0 of Object . values ( tasks ) ) {
if ( isInProcessTeammateTask ( task_0 ) && task_0 . status === 'running' ) {
if ( task_0 . progress ? . tokenCount ) {
teammateTokens += task_0 . progress . tokenCount ;
}
}
}
}
// Stale read of the refs for showBtwTip below — we're off the 50ms clock
// so this only updates when props/app state change, which is sufficient for
// a coarse 30s threshold.
const elapsedSnapshot = pauseStartTimeRef . current !== null ? pauseStartTimeRef . current - loadingStartTimeRef . current - totalPausedMsRef.current : Date.now ( ) - loadingStartTimeRef . current - totalPausedMsRef . current ;
// Leader token count for TeammateSpinnerTree — read raw (non-animated) from
// the ref. The tree is only shown when teammates are running; teammate
// progress updates to s.tasks trigger re-renders that keep this fresh.
const leaderTokenCount = Math . round ( responseLengthRef . current / 4 ) ;
const defaultColor : keyof Theme = 'claude' ;
const defaultShimmerColor = 'claudeShimmer' ;
const messageColor = overrideColor ? ? defaultColor ;
const shimmerColor = overrideShimmerColor ? ? defaultShimmerColor ;
// Compute TTFT string here (off the 50ms animation clock) and pass to
// SpinnerAnimationRow so it folds into the `(thought for Ns · ...)` status
// line instead of taking a separate row. apiMetricsRef is a ref so this
// doesn't trigger re-renders; we pick up updates on the parent's ~25x/turn
// re-render cadence, same as the old ApiMetricsLine did.
let ttftText : string | null = null ;
2026-03-31 22:21:35 +08:00
if ( ( "external" as string ) === 'ant' && apiMetricsRef ? . current && apiMetricsRef . current . length > 0 ) {
2026-03-31 19:22:47 +08:00
ttftText = computeTtftText ( apiMetricsRef . current ) ;
}
// When leader is idle but teammates are running (and we're viewing the leader),
// show a static dim idle display instead of the animated spinner — otherwise
// useStalledAnimation detects no new tokens after 3s and turns the spinner red.
if ( leaderIsIdle && hasRunningTeammates && ! foregroundedTeammate ) {
return < Box flexDirection = "column" width = "100%" alignItems = "flex-start" >
< Box flexDirection = "row" flexWrap = "wrap" marginTop = { 1 } width = "100%" >
< Text dimColor >
{ TEARDROP_ASTERISK } Idle
{ ! allIdle && ' · teammates running' }
< / Text >
< / Box >
{ showSpinnerTree && < TeammateSpinnerTree selectedIndex = { selectedIPAgentIndex } isInSelectionMode = { viewSelectionMode === 'selecting-agent' } allIdle = { allIdle } leaderTokenCount = { leaderTokenCount } leaderIdleText = "Idle" / > }
< / Box > ;
}
// When viewing an idle teammate, show static idle display instead of animated spinner
if ( foregroundedTeammate ? . isIdle ) {
const idleText = allIdle ? ` ${ TEARDROP_ASTERISK } Worked for ${ formatDuration ( Date . now ( ) - foregroundedTeammate . startTime ) } ` : ` ${ TEARDROP_ASTERISK } Idle ` ;
return < Box flexDirection = "column" width = "100%" alignItems = "flex-start" >
< Box flexDirection = "row" flexWrap = "wrap" marginTop = { 1 } width = "100%" >
< Text dimColor > { idleText } < / Text >
< / Box >
{ showSpinnerTree && hasRunningTeammates && < TeammateSpinnerTree selectedIndex = { selectedIPAgentIndex } isInSelectionMode = { viewSelectionMode === 'selecting-agent' } allIdle = { allIdle } leaderVerb = { leaderIsIdle ? undefined : leaderVerb } leaderIdleText = { leaderIsIdle ? 'Idle' : undefined } leaderTokenCount = { leaderTokenCount } / > }
< / Box > ;
}
// Time-based tip overrides: coarse thresholds so a stale ref read (we're
// off the 50ms clock) is fine. Other triggers (mode change, setMessages)
// cause re-renders that refresh this in practice.
let contextTipsActive = false ;
const tipsEnabled = settings . spinnerTipsEnabled !== false ;
const showClearTip = tipsEnabled && elapsedSnapshot > 1 _800_000 ;
const showBtwTip = tipsEnabled && elapsedSnapshot > 30 _000 && ! getGlobalConfig ( ) . btwUseCount ;
const effectiveTip = contextTipsActive ? undefined : showClearTip && ! nextTask ? 'Use /clear to start fresh when switching topics and free up context' : showBtwTip && ! nextTask ? "Use /btw to ask a quick side question without interrupting Claude's current work" : spinnerTip ;
// Budget text (ant-only) — shown above the tip line
let budgetText : string | null = null ;
if ( feature ( 'TOKEN_BUDGET' ) ) {
const budget = getCurrentTurnTokenBudget ( ) ;
if ( budget !== null && budget > 0 ) {
const tokens = getTurnOutputTokens ( ) ;
if ( tokens >= budget ) {
budgetText = ` Target: ${ formatNumber ( tokens ) } used ( ${ formatNumber ( budget ) } min ${ figures . tick } ) ` ;
} else {
const pct = Math . round ( tokens / budget * 100 ) ;
const remaining = budget - tokens ;
const rate = elapsedSnapshot > 5000 && tokens >= 2000 ? tokens / elapsedSnapshot : 0 ;
const eta = rate > 0 ? ` \ u00B7 ~ ${ formatDuration ( remaining / rate , {
mostSignificantOnly : true
} ) } ` : '';
budgetText = ` Target: ${ formatNumber ( tokens ) } / ${ formatNumber ( budget ) } ( ${ pct } %) ${ eta } ` ;
}
}
}
return < Box flexDirection = "column" width = "100%" alignItems = "flex-start" >
< SpinnerAnimationRow mode = { mode } reducedMotion = { reducedMotion } hasActiveTools = { hasActiveTools } responseLengthRef = { responseLengthRef } message = { message } messageColor = { messageColor } shimmerColor = { shimmerColor } overrideColor = { overrideColor } loadingStartTimeRef = { loadingStartTimeRef } totalPausedMsRef = { totalPausedMsRef } pauseStartTimeRef = { pauseStartTimeRef } spinnerSuffix = { spinnerSuffix } verbose = { verbose } columns = { columns } hasRunningTeammates = { hasRunningTeammates } teammateTokens = { teammateTokens } foregroundedTeammate = { foregroundedTeammate } leaderIsIdle = { leaderIsIdle } thinkingStatus = { thinkingStatus } effortSuffix = { effortSuffix } / >
{ showSpinnerTree && hasRunningTeammates ? < TeammateSpinnerTree selectedIndex = { selectedIPAgentIndex } isInSelectionMode = { viewSelectionMode === 'selecting-agent' } allIdle = { allIdle } leaderVerb = { leaderIsIdle ? undefined : leaderVerb } leaderIdleText = { leaderIsIdle ? 'Idle' : undefined } leaderTokenCount = { leaderTokenCount } / > : showExpandedTodos && tasksV2 && tasksV2 . length > 0 ? < Box width = "100%" flexDirection = "column" >
< MessageResponse >
< TaskListV2 tasks = { tasksV2 } / >
< / MessageResponse >
< / Box > : nextTask || effectiveTip || budgetText ?
// IMPORTANT: we need this width="100%" to avoid an Ink bug where the
// tip gets duplicated over and over while the spinner is running if
// the terminal is very small. TODO: fix this in Ink.
< Box width = "100%" flexDirection = "column" >
{ budgetText && < MessageResponse >
< Text dimColor > { budgetText } < / Text >
< / MessageResponse > }
{ ( nextTask || effectiveTip ) && < MessageResponse >
< Text dimColor >
{ nextTask ? ` Next: ${ nextTask . subject } ` : ` Tip: ${ effectiveTip } ` }
< / Text >
< / MessageResponse > }
< / Box > : null }
< / Box > ;
}
// Brief/assistant mode spinner: single status line. PromptInput drops its
// own marginTop when isBriefOnly is active, so this component owns the
// 2-row footprint between messages and input. Footprint is [blank, content]
// — one blank row above (breathing room under the messages list), spinner
// flush against the input bar. PromptInput's absolute-positioned
// Notifications overlay compensates with marginTop=-2 in brief mode
// (PromptInput.tsx:~2928) so it floats into the blank row above the
// spinner, not over the spinner content. Paired with BriefIdleStatus which
// keeps the same footprint when idle.
type BriefSpinnerProps = {
mode : SpinnerMode ;
overrideMessage? : string | null ;
} ;
function BriefSpinner ( t0 ) {
const $ = _c ( 31 ) ;
const {
mode ,
overrideMessage
} = t0 ;
const settings = useSettings ( ) ;
const reducedMotion = settings . prefersReducedMotion ? ? false ;
const [ randomVerb ] = useState ( _temp4 ) ;
const verb = overrideMessage ? ? randomVerb ;
const connStatus = useAppState ( _temp5 ) ;
let t1 ;
let t2 ;
if ( $ [ 0 ] !== mode ) {
t1 = ( ) = > {
const operationId = "spinner-" + mode ;
activityManager . startCLIActivity ( operationId ) ;
return ( ) = > {
activityManager . endCLIActivity ( operationId ) ;
} ;
} ;
t2 = [ mode ] ;
$ [ 0 ] = mode ;
$ [ 1 ] = t1 ;
$ [ 2 ] = t2 ;
} else {
t1 = $ [ 1 ] ;
t2 = $ [ 2 ] ;
}
useEffect ( t1 , t2 ) ;
const [ , time ] = useAnimationFrame ( reducedMotion ? null : 120 ) ;
const runningCount = useAppState ( _temp6 ) ;
const showConnWarning = connStatus === "reconnecting" || connStatus === "disconnected" ;
const connText = connStatus === "reconnecting" ? "Reconnecting" : "Disconnected" ;
const dotFrame = Math . floor ( time / 300 ) % 3 ;
let t3 ;
if ( $ [ 3 ] !== dotFrame || $ [ 4 ] !== reducedMotion ) {
t3 = reducedMotion ? "\u2026 " : "." . repeat ( dotFrame + 1 ) . padEnd ( 3 ) ;
$ [ 3 ] = dotFrame ;
$ [ 4 ] = reducedMotion ;
$ [ 5 ] = t3 ;
} else {
t3 = $ [ 5 ] ;
}
const dots = t3 ;
let t4 ;
if ( $ [ 6 ] !== verb ) {
t4 = stringWidth ( verb ) ;
$ [ 6 ] = verb ;
$ [ 7 ] = t4 ;
} else {
t4 = $ [ 7 ] ;
}
const verbWidth = t4 ;
let t5 ;
if ( $ [ 8 ] !== reducedMotion || $ [ 9 ] !== showConnWarning || $ [ 10 ] !== time || $ [ 11 ] !== verb || $ [ 12 ] !== verbWidth ) {
const glimmerIndex = reducedMotion || showConnWarning ? - 100 : computeGlimmerIndex ( Math . floor ( time / SHIMMER_INTERVAL_MS ) , verbWidth ) ;
t5 = computeShimmerSegments ( verb , glimmerIndex ) ;
$ [ 8 ] = reducedMotion ;
$ [ 9 ] = showConnWarning ;
$ [ 10 ] = time ;
$ [ 11 ] = verb ;
$ [ 12 ] = verbWidth ;
$ [ 13 ] = t5 ;
} else {
t5 = $ [ 13 ] ;
}
const {
before ,
shimmer ,
after
} = t5 ;
const {
columns
} = useTerminalSize ( ) ;
const rightText = runningCount > 0 ? ` ${ runningCount } in background ` : "" ;
let t6 ;
if ( $ [ 14 ] !== connText || $ [ 15 ] !== showConnWarning || $ [ 16 ] !== verbWidth ) {
t6 = showConnWarning ? stringWidth ( connText ) : verbWidth ;
$ [ 14 ] = connText ;
$ [ 15 ] = showConnWarning ;
$ [ 16 ] = verbWidth ;
$ [ 17 ] = t6 ;
} else {
t6 = $ [ 17 ] ;
}
const leftWidth = t6 + 3 ;
const pad = Math . max ( 1 , columns - 2 - leftWidth - stringWidth ( rightText ) ) ;
let t7 ;
if ( $ [ 18 ] !== after || $ [ 19 ] !== before || $ [ 20 ] !== connText || $ [ 21 ] !== dots || $ [ 22 ] !== shimmer || $ [ 23 ] !== showConnWarning ) {
t7 = showConnWarning ? < Text color = "error" > { connText + dots } < / Text > : < > { before ? < Text dimColor = { true } > { before } < / Text > : null } { shimmer ? < Text > { shimmer } < / Text > : null } { after ? < Text dimColor = { true } > { after } < / Text > : null } < Text dimColor = { true } > { dots } < / Text > < / > ;
$ [ 18 ] = after ;
$ [ 19 ] = before ;
$ [ 20 ] = connText ;
$ [ 21 ] = dots ;
$ [ 22 ] = shimmer ;
$ [ 23 ] = showConnWarning ;
$ [ 24 ] = t7 ;
} else {
t7 = $ [ 24 ] ;
}
let t8 ;
if ( $ [ 25 ] !== pad || $ [ 26 ] !== rightText ) {
t8 = rightText ? < > < Text > { " " . repeat ( pad ) } < / Text > < Text color = "subtle" > { rightText } < / Text > < / > : null ;
$ [ 25 ] = pad ;
$ [ 26 ] = rightText ;
$ [ 27 ] = t8 ;
} else {
t8 = $ [ 27 ] ;
}
let t9 ;
if ( $ [ 28 ] !== t7 || $ [ 29 ] !== t8 ) {
t9 = < Box flexDirection = "row" width = "100%" marginTop = { 1 } paddingLeft = { 2 } > { t7 } { t8 } < / Box > ;
$ [ 28 ] = t7 ;
$ [ 29 ] = t8 ;
$ [ 30 ] = t9 ;
} else {
t9 = $ [ 30 ] ;
}
return t9 ;
}
// Idle placeholder for brief mode. Same 2-row [blank, content] footprint
// as BriefSpinner so the input bar never jumps when toggling between
// working/idle/disconnected. See BriefSpinner's comment for the
// Notifications overlay coupling.
function _temp6 ( s_0 ) {
return count ( Object . values ( s_0 . tasks ) , isBackgroundTask ) + s_0 . remoteBackgroundTaskCount ;
}
function _temp5 ( s ) {
return s . remoteConnectionStatus ;
}
function _temp4() {
return sample ( getSpinnerVerbs ( ) ) ? ? "Working" ;
}
export function BriefIdleStatus() {
const $ = _c ( 9 ) ;
const connStatus = useAppState ( _temp7 ) ;
const runningCount = useAppState ( _temp8 ) ;
const {
columns
} = useTerminalSize ( ) ;
const showConnWarning = connStatus === "reconnecting" || connStatus === "disconnected" ;
const connText = connStatus === "reconnecting" ? "Reconnecting\u2026" : "Disconnected" ;
const leftText = showConnWarning ? connText : "" ;
const rightText = runningCount > 0 ? ` ${ runningCount } in background ` : "" ;
if ( ! leftText && ! rightText ) {
let t0 ;
if ( $ [ 0 ] === Symbol . for ( "react.memo_cache_sentinel" ) ) {
t0 = < Box height = { 2 } / > ;
$ [ 0 ] = t0 ;
} else {
t0 = $ [ 0 ] ;
}
return t0 ;
}
const pad = Math . max ( 1 , columns - 2 - stringWidth ( leftText ) - stringWidth ( rightText ) ) ;
let t0 ;
if ( $ [ 1 ] !== leftText ) {
t0 = leftText ? < Text color = "error" > { leftText } < / Text > : null ;
$ [ 1 ] = leftText ;
$ [ 2 ] = t0 ;
} else {
t0 = $ [ 2 ] ;
}
let t1 ;
if ( $ [ 3 ] !== pad || $ [ 4 ] !== rightText ) {
t1 = rightText ? < > < Text > { " " . repeat ( pad ) } < / Text > < Text color = "subtle" > { rightText } < / Text > < / > : null ;
$ [ 3 ] = pad ;
$ [ 4 ] = rightText ;
$ [ 5 ] = t1 ;
} else {
t1 = $ [ 5 ] ;
}
let t2 ;
if ( $ [ 6 ] !== t0 || $ [ 7 ] !== t1 ) {
t2 = < Box marginTop = { 1 } paddingLeft = { 2 } > < Text > { t0 } { t1 } < / Text > < / Box > ;
$ [ 6 ] = t0 ;
$ [ 7 ] = t1 ;
$ [ 8 ] = t2 ;
} else {
t2 = $ [ 8 ] ;
}
return t2 ;
}
function _temp8 ( s_0 ) {
return count ( Object . values ( s_0 . tasks ) , isBackgroundTask ) + s_0 . remoteBackgroundTaskCount ;
}
function _temp7 ( s ) {
return s . remoteConnectionStatus ;
}
export function Spinner() {
const $ = _c ( 8 ) ;
const settings = useSettings ( ) ;
const reducedMotion = settings . prefersReducedMotion ? ? false ;
const [ ref , time ] = useAnimationFrame ( reducedMotion ? null : 120 ) ;
if ( reducedMotion ) {
let t0 ;
if ( $ [ 0 ] === Symbol . for ( "react.memo_cache_sentinel" ) ) {
t0 = < Text color = "text" > ● < / Text > ;
$ [ 0 ] = t0 ;
} else {
t0 = $ [ 0 ] ;
}
let t1 ;
if ( $ [ 1 ] !== ref ) {
t1 = < Box ref = { ref } flexWrap = "wrap" height = { 1 } width = { 2 } > { t0 } < / Box > ;
$ [ 1 ] = ref ;
$ [ 2 ] = t1 ;
} else {
t1 = $ [ 2 ] ;
}
return t1 ;
}
const frame = Math . floor ( time / 120 ) % SPINNER_FRAMES . length ;
const t0 = SPINNER_FRAMES [ frame ] ;
let t1 ;
if ( $ [ 3 ] !== t0 ) {
t1 = < Text color = "text" > { t0 } < / Text > ;
$ [ 3 ] = t0 ;
$ [ 4 ] = t1 ;
} else {
t1 = $ [ 4 ] ;
}
let t2 ;
if ( $ [ 5 ] !== ref || $ [ 6 ] !== t1 ) {
t2 = < Box ref = { ref } flexWrap = "wrap" height = { 1 } width = { 2 } > { t1 } < / Box > ;
$ [ 5 ] = ref ;
$ [ 6 ] = t1 ;
$ [ 7 ] = t2 ;
} else {
t2 = $ [ 7 ] ;
}
return t2 ;
}
function findNextPendingTask ( tasks : Task [ ] | undefined ) : Task | undefined {
if ( ! tasks ) {
return undefined ;
}
const pendingTasks = tasks . filter ( t = > t . status === 'pending' ) ;
if ( pendingTasks . length === 0 ) {
return undefined ;
}
const unresolvedIds = new Set ( tasks . filter ( t = > t . status !== 'completed' ) . map ( t = > t . id ) ) ;
return pendingTasks . find ( t = > ! t . blockedBy . some ( id = > unresolvedIds . has ( id ) ) ) ? ? pendingTasks [ 0 ] ;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJCb3giLCJUZXh0IiwiUmVhY3QiLCJ1c2VFZmZlY3QiLCJ1c2VNZW1vIiwidXNlUmVmIiwidXNlU3RhdGUiLCJjb21wdXRlR2xpbW1lckluZGV4IiwiY29tcHV0ZVNoaW1tZXJTZWdtZW50cyIsIlNISU1NRVJfSU5URVJWQUxfTVMiLCJmZWF0dXJlIiwiZ2V0S2Fpcm9zQWN0aXZlIiwiZ2V0VXNlck1zZ09wdEluIiwiZ2V0RmVhdHVyZVZhbHVlX0NBQ0hFRF9NQVlfQkVfU1RBTEUiLCJpc0VudlRydXRoeSIsImNvdW50Iiwic2FtcGxlIiwiZm9ybWF0RHVyYXRpb24iLCJmb3JtYXROdW1iZXIiLCJmb3JtYXRTZWNvbmRzU2hvcnQiLCJUaGVtZSIsImFjdGl2aXR5TWFuYWdlciIsImdldFNwaW5uZXJWZXJicyIsIk1lc3NhZ2VSZXNwb25zZSIsIlRhc2tMaXN0VjIiLCJ1c2VUYXNrc1YyIiwiVGFzayIsInVzZUFwcFN0YXRlIiwidXNlVGVybWluYWxTaXplIiwic3RyaW5nV2lkdGgiLCJnZXREZWZhdWx0Q2hhcmFjdGVycyIsIlNwaW5uZXJNb2RlIiwiU3Bpbm5lckFuaW1hdGlvblJvdyIsInVzZVNldHRpbmdzIiwiaXNJblByb2Nlc3NUZWFtbWF0ZVRhc2siLCJpc0JhY2tncm91bmRUYXNrIiwiZ2V0QWxsSW5Qcm9jZXNzVGVhbW1hdGVUYXNrcyIsImdldEVmZm9ydFN1ZmZpeCIsImdldE1haW5Mb29wTW9kZWwiLCJnZXRWaWV3ZWRUZWFtbWF0ZVRhc2siLCJURUFSRFJPUF9BU1RFUklTSyIsImZpZ3VyZXMiLCJnZXRDdXJyZW50VHVyblRva2VuQnVkZ2V0IiwiZ2V0VHVybk91dHB1dFRva2VucyIsIlRlYW1tYXRlU3Bpbm5lclRyZWUiLCJ1c2VBbmltYXRpb25GcmFtZSIsImdldEdsb2JhbENvbmZpZyIsIkRFRkFVTFRfQ0hBUkFDVEVSUyIsIlNQSU5ORVJfRlJBTUVTIiwicmV2ZXJzZSIsIlByb3BzIiwibW9kZSIsImxvYWRpbmdTdGFydFRpbWVSZWYiLCJSZWZPYmplY3QiLCJ0b3RhbFBhdXNlZE1zUmVmIiwicGF1c2VTdGFydFRpbWVSZWYiLCJzcGlubmVyVGlwIiwicmVzcG9uc2VMZW5ndGhSZWYiLCJvdmVycmlkZUNvbG9yIiwib3ZlcnJpZGVTaGltbWVyQ29sb3IiLCJvdmVycmlkZU1lc3NhZ2UiLCJzcGlubmVyU3VmZml4IiwidmVyYm9zZSIsImhhc0FjdGl2ZVRvb2xzIiwibGVhZGVySXNJZGxlIiwiU3Bpbm5lcldpdGhWZXJiIiwicHJvcHMiLCJSZWFjdE5vZGUiLCJpc0JyaWVmT25seSIsInMiLCJ2aWV3aW5nQWdlbnRUYXNrSWQiLCJicmllZkVudkVuYWJsZWQiLCJwcm9jZXNzIiwiZW52IiwiQ0xBVURFX0NPREVfQlJJRUYiLCJTcGlubmVyV2l0aFZlcmJJbm5lciIsInNldHRpbmdzIiwicmVkdWNlZE1vdGlvbiIsInByZWZlcnNSZWR1Y2VkTW90aW9uIiwidGFza3MiLCJleHBhbmRlZFZpZXciLCJzaG93RXhwYW5kZWRUb2RvcyIsInNob3dTcGlubmVyVHJlZSIsInNlbGVjdGVkSVBBZ2VudEluZGV4Iiwidmlld1NlbGVjdGlvbk1vZGUiLCJmb3JlZ3JvdW5kZWRUZWFtbWF0ZSIsInVuZGVmaW5lZCIsImNvbHVtbnMiLCJ0YXNrc1YyIiwidGhpbmtpbmdTdGF0dXMiLCJzZXRUaGlua2luZ1N0YXR1cyIsInRoaW5raW5nU3RhcnRSZWYiLCJzaG93RHVyYXRpb25UaW1lciIsIlJldHVyblR5cGUiLCJzZXRUaW1lb3V0IiwiY2xlYXJTdGF0dXNUaW1lciIsImN1cnJlbnQiLCJEYXRlIiwibm93IiwiZHVyYXRpb24iLCJlbGFwc2VkIiwicmVtYWluaW5nVGhpbmtpbmdUaW1lIiwiTWF0aCIsIm1heCIsInNob3dEdXJhdGlvbiIsImNsZWFyVGltZW91dCIsImN1cnJlbnRUb2RvIiwiZmluZCIsInRhc2siLCJzdGF0dXMiLCJuZXh0VGFzayIsImZpbmROZXh0UGVuZGluZ1Rhc2siLCJyYW5kb21WZXJiIiwibGVhZGVyVmVyYiIsImFjdGl2ZUZvcm0iLCJzdWJqZWN0IiwiZWZmZWN0aXZlVmVyYiIsImlzSWRsZSIsInNwaW5uZXJWZXJiIiwibWVzc2FnZSIsIm9wZXJhdGlvbklkIiwic3RhcnRDTElBY3Rpdml0eSIsImVuZENMSUFjdGl2aXR5IiwiZWZmb3J0VmFsdWUiLCJlZmZvcnRTdWZmaXgiLCJydW5uaW5nVGVhbW1hdGVzIiwiZmlsdGVyIiwidCIsImhhc1J1bm5pbmdUZWFtbWF0ZXMiLCJsZW5ndGgiLCJhbGxJZGxlIiwiZXZlcnkiLCJ0ZWFtbWF0ZVRva2VucyIsIk9iamVjdCIsInZhbHVlcyIsInByb2dyZXNzIiwidG9rZW5Db3VudCIsImVsYXBzZWRTbmFwc2hvdCIsImxlYWRlclRva2VuQ291bnQiLCJyb3VuZCIsImRlZmF1bHRDb2xvciIsImRlZmF1bHRTaGltbWVyQ29sb3IiLCJtZXNzYWdlQ29sb3IiLCJzaGltbWVyQ29sb3IiLCJ0dGZ0VGV4dCIsImFwaU1ldHJpY3NSZWYiLCJjb21wdXRlVHRmdFRleHQiLCJpZGxlVGV4dCIsInN0YXJ0VGltZSIsImNvbnRleHRUaXBzQWN0aXZlIiwidGlwc0VuYWJsZWQiLCJzcGlubmVyVGlwc0VuYWJsZWQiLCJzaG93Q2xlYXJUaXAiLCJzaG93QnR3VGlwIiwiYnR3VXNlQ291bnQiLCJlZmZlY3RpdmVUaXAiLCJidWRnZXRUZXh0IiwiYnVkZ2V0IiwidG9rZW5zIiwidGljayIsInBjdCIsInJlbWFpbmluZyIsInJhdGUiLCJldGEiLCJtb3N0U2lnbmlmaWNhbnRPbmx5IiwiQnJpZWZTcGlubmVyUHJvcHMiLCJCcmllZlNwaW5uZXIiLCJ0MCIsIiQiLCJfYyIsIl90ZW1wNCIsInZlcmIiLCJjb25uU3RhdHVzIiwiX3RlbXA1IiwidDEiLCJ0MiIsInRpbWUiLCJydW5uaW5nQ291bnQiLCJfdGVtcDYiLCJzaG93Q29ubldhcm5pbmciLCJjb25uVGV4dCIsImRvdEZyYW1lIiwiZmxvb3IiLCJ0MyIsInJlcGVhdCIsInBhZEVuZCIsImRvdHMiLCJ0NCIsInZlcmJXaWR0aCIsInQ1IiwiZ2xpbW1lckluZGV4IiwiYmVmb3JlIiwic2hpbW1lciIsImFmdGVyIiwicmlnaHRUZXh0IiwidDYiLCJsZWZ0V2lkdGgiLCJwYWQiLCJ0NyIsInQ4IiwidDkiLCJzXzAiLCJyZW1vdGVCYWNrZ3JvdW5kVGFza0NvdW50IiwicmVtb3RlQ29ubmVjdGlvblN0YXR1cyIsIkJyaWVmSWRsZVN0YXR1cyIsIl90ZW1wNyIsIl90ZW1wOCIsImxlZnRUZXh0IiwiU3ltYm9sIiwiZm9yIiwiU3Bpbm5lciIsInJlZiIsImZyYW1lIiwicGVuZGluZ1Rhc2tzIiwidW5yZXNvbHZlZElkcyIsIlNldCIsIm1hcCIsImlkIiwiYmxvY2tlZEJ5Iiwic29tZSIsImhhcyJdLCJzb3VyY2VzIjpbIlNwaW5uZXIudHN