44 * SPDX-License-Identifier: Apache-2.0
55 */
66
7- import type React from 'react' ;
7+ import React from 'react' ;
88import { Box , Text } from 'ink' ;
99import { theme } from '../../semantic-colors.js' ;
1010import { formatDuration } from '../../utils/formatters.js' ;
11- import type { GoalStatusKind } from '../../types.js' ;
11+ import { isTerminalGoalStatusKind , type GoalStatusKind } from '../../types.js' ;
1212
1313interface GoalStatusMessageProps {
1414 kind : GoalStatusKind ;
@@ -20,7 +20,11 @@ interface GoalStatusMessageProps {
2020
2121const pluralTurns = ( n : number ) => ( n === 1 ? 'turn' : 'turns' ) ;
2222
23- export const GoalStatusMessage : React . FC < GoalStatusMessageProps > = ( {
23+ function assertNeverGoalStatusKind ( kind : never ) : never {
24+ throw new Error ( `Unexpected goal status kind: ${ kind } ` ) ;
25+ }
26+
27+ const GoalStatusMessageInternal : React . FC < GoalStatusMessageProps > = ( {
2428 kind,
2529 condition,
2630 iterations,
@@ -81,13 +85,20 @@ export const GoalStatusMessage: React.FC<GoalStatusMessageProps> = ({
8185 prefixColor : theme . text . secondary ,
8286 title : 'Goal cleared' ,
8387 } ;
88+ case 'failed' :
89+ return {
90+ prefix : '✖' ,
91+ prefixColor : theme . status . error ,
92+ title : 'Goal could not be achieved' ,
93+ } ;
8494 case 'aborted' :
85- default :
8695 return {
8796 prefix : '!' ,
8897 prefixColor : theme . status . warning ,
8998 title : 'Goal aborted' ,
9099 } ;
100+ default :
101+ return assertNeverGoalStatusKind ( kind ) ;
91102 }
92103 } ) ( ) ;
93104
@@ -126,7 +137,8 @@ export const GoalStatusMessage: React.FC<GoalStatusMessageProps> = ({
126137 < Text wrap = "wrap" > { condition } </ Text >
127138 </ Box >
128139 </ Box >
129- { /* `lastReason` is shown on terminal cards (achieved / aborted) so
140+ { /* `lastReason` is shown on terminal cards (achieved / aborted /
141+ failed) so
130142 the final summary records *why* the judge ruled the goal complete
131143 or why the loop gave up. Skipped for `cleared` because user-driven
132144 clears don't carry a judge reason.
@@ -136,7 +148,7 @@ export const GoalStatusMessage: React.FC<GoalStatusMessageProps> = ({
136148 flex-row variant hangs the continuation at the value column's
137149 left edge (≈12 cols of empty space, easily mistaken for a blank
138150 line). One Text + natural wrap keeps the continuation flush. */ }
139- { ( kind === 'achieved' || kind === 'aborted' ) && lastReason ?. trim ( ) ? (
151+ { isTerminalGoalStatusKind ( kind ) && lastReason ?. trim ( ) ? (
140152 < Text color = { theme . text . secondary } wrap = "wrap" >
141153 Last check: { lastReason . trim ( ) }
142154 </ Text >
@@ -145,3 +157,5 @@ export const GoalStatusMessage: React.FC<GoalStatusMessageProps> = ({
145157 </ Box >
146158 ) ;
147159} ;
160+
161+ export const GoalStatusMessage = React . memo ( GoalStatusMessageInternal ) ;
0 commit comments