diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx
index 174746d314..bb9b504bfc 100644
--- a/packages/cli/src/ui/components/Composer.tsx
+++ b/packages/cli/src/ui/components/Composer.tsx
@@ -206,11 +206,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
showMinimalBleedThroughRow ||
showShortcutsHint);
- const ambientText = isInteractiveShellWaiting
- ? undefined
- : (showTips ? uiState.currentTip : undefined) ||
- (showWit ? uiState.currentWittyPhrase : undefined);
-
let estimatedStatusLength = 0;
if (
isExperimentalLayout &&
@@ -238,6 +233,32 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
estimatedStatusLength = 20; // "↑ Action required"
}
+ const ambientText = (() => {
+ if (isInteractiveShellWaiting) return undefined;
+
+ // Try Tip first
+ if (showTips && uiState.currentTip) {
+ if (
+ estimatedStatusLength + uiState.currentTip.length + 5 <=
+ terminalWidth
+ ) {
+ return uiState.currentTip;
+ }
+ }
+
+ // Fallback to Wit
+ if (showWit && uiState.currentWittyPhrase) {
+ if (
+ estimatedStatusLength + uiState.currentWittyPhrase.length + 5 <=
+ terminalWidth
+ ) {
+ return uiState.currentWittyPhrase;
+ }
+ }
+
+ return undefined;
+ })();
+
const estimatedAmbientLength = ambientText?.length || 0;
const willCollideAmbient =
estimatedStatusLength + estimatedAmbientLength + 5 > terminalWidth;
@@ -265,11 +286,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
marginLeft={1}
marginRight={1}
>
- {isExperimentalLayout ? (
-
- ) : (
- showShortcutsHint &&
- )}
+
);
}
diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx
index c883c95938..5eff010cb6 100644
--- a/packages/cli/src/ui/components/LoadingIndicator.tsx
+++ b/packages/cli/src/ui/components/LoadingIndicator.tsx
@@ -81,8 +81,8 @@ export const LoadingIndicator: React.FC = ({
wittyPhrase &&
primaryText === GENERIC_WORKING_LABEL ? (
-
- {wittyPhrase}
+
+ {wittyPhrase} :)
) : null;
diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
index d74b8f060a..ab7431da7a 100644
--- a/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
+++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.tsx
@@ -251,7 +251,7 @@ describe('usePhraseCycler', () => {
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');
unmount();
- expect(clearIntervalSpy).toHaveBeenCalledOnce();
+ expect(clearIntervalSpy).toHaveBeenCalled();
});
it('should use custom phrases when provided', async () => {
diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts
index 68ec573214..1b82336afe 100644
--- a/packages/cli/src/ui/hooks/usePhraseCycler.ts
+++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts
@@ -8,7 +8,8 @@ import { useState, useEffect, useRef } from 'react';
import { INFORMATIVE_TIPS } from '../constants/tips.js';
import { WITTY_LOADING_PHRASES } from '../constants/wittyPhrases.js';
-export const PHRASE_CHANGE_INTERVAL_MS = 15000;
+export const PHRASE_CHANGE_INTERVAL_MS = 10000;
+export const WITTY_PHRASE_CHANGE_INTERVAL_MS = 5000;
export const INTERACTIVE_SHELL_WAITING_PHRASE =
'! Shell awaiting input (Tab to focus)';
@@ -39,18 +40,29 @@ export const usePhraseCycler = (
string | undefined
>(undefined);
- const phraseIntervalRef = useRef(null);
- const lastChangeTimeRef = useRef(0);
+ const tipIntervalRef = useRef(null);
+ const wittyIntervalRef = useRef(null);
+ const lastTipChangeTimeRef = useRef(0);
+ const lastWittyChangeTimeRef = useRef(0);
const lastSelectedTipRef = useRef(undefined);
const lastSelectedWittyPhraseRef = useRef(undefined);
const MIN_TIP_DISPLAY_TIME_MS = 10000;
+ const MIN_WIT_DISPLAY_TIME_MS = 5000;
useEffect(() => {
// Always clear on re-run
- if (phraseIntervalRef.current) {
- clearInterval(phraseIntervalRef.current);
- phraseIntervalRef.current = null;
- }
+ const clearTimers = () => {
+ if (tipIntervalRef.current) {
+ clearInterval(tipIntervalRef.current);
+ tipIntervalRef.current = null;
+ }
+ if (wittyIntervalRef.current) {
+ clearInterval(wittyIntervalRef.current);
+ wittyIntervalRef.current = null;
+ }
+ };
+
+ clearTimers();
if (shouldShowFocusHint || isWaiting) {
// These are handled by the return value directly for immediate feedback
@@ -66,69 +78,85 @@ export const usePhraseCycler = (
? customPhrases
: WITTY_LOADING_PHRASES;
- const setRandomPhrases = (force: boolean = false) => {
+ const setRandomTip = (force: boolean = false) => {
+ if (!showTips) {
+ setCurrentTipState(undefined);
+ lastSelectedTipRef.current = undefined;
+ return;
+ }
+
const now = Date.now();
if (
!force &&
- now - lastChangeTimeRef.current < MIN_TIP_DISPLAY_TIME_MS &&
- (lastSelectedTipRef.current || lastSelectedWittyPhraseRef.current)
+ now - lastTipChangeTimeRef.current < MIN_TIP_DISPLAY_TIME_MS &&
+ lastSelectedTipRef.current
) {
- // Sync state if it was cleared by inactivation.
setCurrentTipState(lastSelectedTipRef.current);
+ return;
+ }
+
+ const filteredTips =
+ maxLength !== undefined
+ ? INFORMATIVE_TIPS.filter((p) => p.length <= maxLength)
+ : INFORMATIVE_TIPS;
+
+ if (filteredTips.length > 0) {
+ const selected =
+ filteredTips[Math.floor(Math.random() * filteredTips.length)];
+ setCurrentTipState(selected);
+ lastSelectedTipRef.current = selected;
+ lastTipChangeTimeRef.current = now;
+ }
+ };
+
+ const setRandomWitty = (force: boolean = false) => {
+ if (!showWit) {
+ setCurrentWittyPhraseState(undefined);
+ lastSelectedWittyPhraseRef.current = undefined;
+ return;
+ }
+
+ const now = Date.now();
+ if (
+ !force &&
+ now - lastWittyChangeTimeRef.current < MIN_WIT_DISPLAY_TIME_MS &&
+ lastSelectedWittyPhraseRef.current
+ ) {
setCurrentWittyPhraseState(lastSelectedWittyPhraseRef.current);
return;
}
- const adjustedMaxLength = maxLength;
+ const filteredWitty =
+ maxLength !== undefined
+ ? wittyPhrasesList.filter((p) => p.length <= maxLength)
+ : wittyPhrasesList;
- if (showTips) {
- const filteredTips =
- adjustedMaxLength !== undefined
- ? INFORMATIVE_TIPS.filter((p) => p.length <= adjustedMaxLength)
- : INFORMATIVE_TIPS;
- if (filteredTips.length > 0) {
- const selected =
- filteredTips[Math.floor(Math.random() * filteredTips.length)];
- setCurrentTipState(selected);
- lastSelectedTipRef.current = selected;
- }
- } else {
- setCurrentTipState(undefined);
- lastSelectedTipRef.current = undefined;
+ if (filteredWitty.length > 0) {
+ const selected =
+ filteredWitty[Math.floor(Math.random() * filteredWitty.length)];
+ setCurrentWittyPhraseState(selected);
+ lastSelectedWittyPhraseRef.current = selected;
+ lastWittyChangeTimeRef.current = now;
}
-
- if (showWit) {
- const filteredWitty =
- adjustedMaxLength !== undefined
- ? wittyPhrasesList.filter((p) => p.length <= adjustedMaxLength)
- : wittyPhrasesList;
- if (filteredWitty.length > 0) {
- const selected =
- filteredWitty[Math.floor(Math.random() * filteredWitty.length)];
- setCurrentWittyPhraseState(selected);
- lastSelectedWittyPhraseRef.current = selected;
- }
- } else {
- setCurrentWittyPhraseState(undefined);
- lastSelectedWittyPhraseRef.current = undefined;
- }
-
- lastChangeTimeRef.current = now;
};
// Select initial random phrases or resume previous ones
- setRandomPhrases(false);
+ setRandomTip(false);
+ setRandomWitty(false);
- phraseIntervalRef.current = setInterval(() => {
- setRandomPhrases(true); // Force change on interval
- }, PHRASE_CHANGE_INTERVAL_MS);
+ if (showTips) {
+ tipIntervalRef.current = setInterval(() => {
+ setRandomTip(true);
+ }, PHRASE_CHANGE_INTERVAL_MS);
+ }
- return () => {
- if (phraseIntervalRef.current) {
- clearInterval(phraseIntervalRef.current);
- phraseIntervalRef.current = null;
- }
- };
+ if (showWit) {
+ wittyIntervalRef.current = setInterval(() => {
+ setRandomWitty(true);
+ }, WITTY_PHRASE_CHANGE_INTERVAL_MS);
+ }
+
+ return clearTimers;
}, [
isActive,
isWaiting,