feat(cli): implement customizable witty phrase positioning

- Add ui.wittyPhrasePosition setting (status, inline, ambient)
- Refactor usePhraseCycler to return tips and wit separately
- Implement 'inline' position: append witty phrases in gray after status
- Update status length estimation to account for inline wit
- Replace pause icon with up arrow (↑) for awaiting approval
- Remove 'Tip:' prefix from loading phrases
- Update unit tests and research report
This commit is contained in:
Keith Guerin
2026-02-28 23:25:06 -08:00
parent 5e87ba8be3
commit 3bd36ce4f0
9 changed files with 105 additions and 55 deletions

View File

@@ -59,6 +59,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const isAlternateBuffer = useAlternateBuffer();
const { showApprovalModeIndicator } = uiState;
const newLayoutSetting = settings.merged.ui.newFooterLayout;
const wittyPosition = settings.merged.ui.wittyPhrasePosition;
const isExperimentalLayout = newLayoutSetting !== 'legacy';
const showUiDetails = uiState.cleanUiDetailsVisible;
const suggestionsPosition = isAlternateBuffer ? 'above' : 'below';
@@ -197,15 +198,8 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
const ambientText = isInteractiveShellWaiting
? undefined
: uiState.currentLoadingPhrase;
// Wit often ends with an ellipsis or similar, tips usually don't.
const isAmbientTip =
ambientText &&
!ambientText.includes('…') &&
!ambientText.includes('...') &&
!ambientText.includes('feeling lucky');
const ambientPrefix = isAmbientTip ? 'Tip: ' : '';
: uiState.currentTip ||
(wittyPosition === 'ambient' ? uiState.currentWittyPhrase : undefined);
let estimatedStatusLength = 0;
if (
@@ -225,13 +219,16 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
estimatedStatusLength = hookLabel.length + hookNames.length + 10; // +10 for spinner and spacing
} else if (showLoadingIndicator) {
const thoughtText = uiState.thought?.subject || 'Waiting for model...';
estimatedStatusLength = thoughtText.length + 25; // Spinner(3) + timer(15) + padding
const inlineWittyLength =
wittyPosition === 'inline' && uiState.currentWittyPhrase
? uiState.currentWittyPhrase.length + 1
: 0;
estimatedStatusLength = thoughtText.length + 25 + inlineWittyLength; // Spinner(3) + timer(15) + padding + witty
} else if (hasPendingActionRequired) {
estimatedStatusLength = 35; // " Awaiting user approval..."
estimatedStatusLength = 25; // " Awaiting approval"
}
const estimatedAmbientLength =
ambientPrefix.length + (ambientText?.length || 0);
const estimatedAmbientLength = ambientText?.length || 0;
const willCollideAmbient =
estimatedStatusLength + estimatedAmbientLength + 5 > terminalWidth;
const willCollideShortcuts = estimatedStatusLength + 45 > terminalWidth; // Assume worst-case shortcut hint is 45 chars
@@ -263,7 +260,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
return (
<Box flexDirection="row" justifyContent="flex-end" marginLeft={1}>
<Text color={theme.text.secondary} wrap="truncate-end">
{ambientPrefix}
{ambientText}
</Text>
</Box>
@@ -322,13 +318,13 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
elapsedTime={uiState.elapsedTime}
forceRealStatusOnly={isExperimentalLayout}
showCancelAndTimer={!isExperimentalLayout}
wittyPhrase={uiState.currentWittyPhrase}
wittyPosition={wittyPosition}
/>
);
}
if (hasPendingActionRequired) {
return (
<Text color={theme.status.warning}> Awaiting user approval...</Text>
);
return <Text color={theme.status.warning}> Awaiting approval</Text>;
}
return null;
};

View File

@@ -18,6 +18,8 @@ import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
interface LoadingIndicatorProps {
currentLoadingPhrase?: string;
wittyPhrase?: string;
wittyPosition?: 'status' | 'inline' | 'ambient';
elapsedTime: number;
inline?: boolean;
rightContent?: React.ReactNode;
@@ -29,6 +31,8 @@ interface LoadingIndicatorProps {
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
currentLoadingPhrase,
wittyPhrase,
wittyPosition = 'inline',
elapsedTime,
inline = false,
rightContent,
@@ -57,9 +61,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
: thought?.subject
? (thoughtLabel ?? thought.subject)
: forceRealStatusOnly
? streamingState === StreamingState.Responding
? 'Waiting for model...'
: undefined
? wittyPosition === 'status' && wittyPhrase
? wittyPhrase
: streamingState === StreamingState.Responding
? 'Waiting for model...'
: undefined
: currentLoadingPhrase;
const thinkingIndicator = '';
@@ -69,6 +75,16 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
? `esc to cancel, ${elapsedTime < 60 ? `${elapsedTime}s` : formatDuration(elapsedTime * 1000)}`
: null;
const wittyPhraseNode =
forceRealStatusOnly &&
wittyPosition === 'inline' &&
wittyPhrase &&
primaryText ? (
<Box marginLeft={1}>
<Text color={theme.text.secondary}>{wittyPhrase}</Text>
</Box>
) : null;
if (inline) {
return (
<Box>
@@ -91,6 +107,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
{primaryText}
</Text>
)}
{wittyPhraseNode}
{cancelAndTimerContent && (
<>
<Box flexShrink={0} width={1} />
@@ -129,6 +146,7 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
{primaryText}
</Text>
)}
{wittyPhraseNode}
{!isNarrow && cancelAndTimerContent && (
<>
<Box flexShrink={0} width={1} />