feat(cli): add thinking indicator tip bar with memory tips (#1410)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user