feat(cli): add thinking indicator tip bar with memory tips (#1410)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Devansh Jain
2026-03-16 16:04:00 -07:00
committed by GitHub
parent aba5ecc62a
commit be41f9f5c7
3 changed files with 76 additions and 20 deletions

View File

@@ -37,6 +37,7 @@ import {
getSnapshot as getSubagentSnapshot,
subscribe as subscribeToSubagents,
} from "../helpers/subagentState.js";
import { getRandomThinkingTip } from "../helpers/thinkingMessages";
import { BlinkingSpinner } from "./BlinkingSpinner.js";
import { colors } from "./colors";
import { InputAssist } from "./InputAssist";
@@ -527,6 +528,8 @@ const StreamingStatus = memo(function StreamingStatus({
const [shimmerOffset, setShimmerOffset] = useState(-3);
const [elapsedMs, setElapsedMs] = useState(0);
const [tipMessage, setTipMessage] = useState("");
const tipInitializedRef = useRef(false);
const streamStartRef = useRef<number | null>(null);
useEffect(() => {
@@ -578,6 +581,18 @@ const StreamingStatus = memo(function StreamingStatus({
setElapsedMs(0);
}, [streaming, visible]);
useEffect(() => {
if (streaming && visible) {
if (!tipInitializedRef.current) {
setTipMessage(getRandomThinkingTip());
tipInitializedRef.current = true;
}
return;
}
tipInitializedRef.current = false;
}, [streaming, visible]);
const estimatedTokens = charsToTokens(tokenCount);
const totalElapsedMs = elapsedBaseMs + elapsedMs;
const shouldShowTokenCount =
@@ -650,33 +665,46 @@ const StreamingStatus = memo(function StreamingStatus({
hintColor(" (") + hintBold("esc") + hintColor(` to interrupt${suffix}`)
);
}, [interruptRequested, statusHintSuffix]);
const tipLineText = useMemo(() => {
return truncateEnd(`⎿ Tip: ${tipMessage}`, statusContentWidth);
}, [tipMessage, statusContentWidth]);
if (!streaming || !visible) {
return null;
}
return (
<Box flexDirection="row" marginBottom={1}>
<Box width={2} flexShrink={0}>
<Text color={colors.status.processing}>
{animate ? <Spinner type="layer" /> : "●"}
</Text>
</Box>
<Box width={statusContentWidth} flexShrink={0} flexDirection="row">
<Box width={messageColumnWidth} flexShrink={0}>
<ShimmerText
boldPrefix={agentName || undefined}
message={thinkingMessage}
shimmerOffset={animate ? shimmerOffset : -3}
wrap="truncate-end"
/>
<Box flexDirection="column" marginBottom={1}>
<Box flexDirection="row">
<Box width={2} flexShrink={0}>
<Text color={colors.status.processing}>
{animate ? <Spinner type="layer" /> : "●"}
</Text>
</Box>
{hintColumnWidth > 0 && (
<Box width={hintColumnWidth} flexShrink={0}>
<Text wrap="truncate-end">{statusHintText}</Text>
<Box width={statusContentWidth} flexShrink={0} flexDirection="row">
<Box width={messageColumnWidth} flexShrink={0}>
<ShimmerText
boldPrefix={agentName || undefined}
message={thinkingMessage}
shimmerOffset={animate ? shimmerOffset : -3}
wrap="truncate-end"
/>
</Box>
)}
<Box flexGrow={1} />
{hintColumnWidth > 0 && (
<Box width={hintColumnWidth} flexShrink={0}>
<Text wrap="truncate-end">{statusHintText}</Text>
</Box>
)}
<Box flexGrow={1} />
</Box>
</Box>
<Box flexDirection="row">
<Box width={2} flexShrink={0} />
<Box width={statusContentWidth} flexShrink={0}>
<Text color={colors.subagent.hint} wrap="truncate-end">
{tipLineText}
</Text>
</Box>
</Box>
</Box>
);

View File

@@ -43,6 +43,14 @@ const THINKING_VERBS = [
"internalizing",
] as const;
export const THINKING_TIPS = [
"Use /remember [instructions] to remember something from the conversation.",
"Use /palace to inspect your agent's memory palace.",
"Use /reflect to launch a background reflection agent to update memory.",
"Use /search [query] to search messages across all agents.",
"Use /init to initialize (or re-init) your agent's memory.",
] as const;
type ThinkingVerb = (typeof THINKING_VERBS)[number];
const PAST_TENSE_VERBS: Record<ThinkingVerb, string> = {
@@ -95,6 +103,11 @@ function getRandomVerb(): string {
return THINKING_VERBS[index] ?? "thinking";
}
function getRandomTip(): string {
const index = Math.floor(Math.random() * THINKING_TIPS.length);
return THINKING_TIPS[index] ?? "";
}
// Get a random thinking verb phrase (e.g., "is thinking", "is processing")
export function getRandomThinkingVerb(): string {
return `is ${getRandomVerb()}`;
@@ -117,3 +130,7 @@ export function getRandomThinkingMessage(agentName?: string | null): string {
// Fallback to capitalized verb if no agent name
return verb.charAt(0).toUpperCase() + verb.slice(1);
}
export function getRandomThinkingTip(): string {
return getRandomTip();
}

View File

@@ -1,5 +1,9 @@
import { describe, expect, test } from "bun:test";
import { getRandomThinkingMessage } from "../../cli/helpers/thinkingMessages";
import {
getRandomThinkingMessage,
getRandomThinkingTip,
THINKING_TIPS,
} from "../../cli/helpers/thinkingMessages";
describe("Thinking messages", () => {
test("returns formatted message with agent name", () => {
@@ -43,4 +47,11 @@ describe("Thinking messages", () => {
// Should have more than 1 unique message (with high probability)
expect(messages.size).toBeGreaterThan(1);
});
test("returns a tip from the configured tip list", () => {
const tip = getRandomThinkingTip();
expect(tip.length).toBeGreaterThan(0);
expect((THINKING_TIPS as readonly string[]).includes(tip)).toBe(true);
});
});