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,
|
getSnapshot as getSubagentSnapshot,
|
||||||
subscribe as subscribeToSubagents,
|
subscribe as subscribeToSubagents,
|
||||||
} from "../helpers/subagentState.js";
|
} from "../helpers/subagentState.js";
|
||||||
|
import { getRandomThinkingTip } from "../helpers/thinkingMessages";
|
||||||
import { BlinkingSpinner } from "./BlinkingSpinner.js";
|
import { BlinkingSpinner } from "./BlinkingSpinner.js";
|
||||||
import { colors } from "./colors";
|
import { colors } from "./colors";
|
||||||
import { InputAssist } from "./InputAssist";
|
import { InputAssist } from "./InputAssist";
|
||||||
@@ -527,6 +528,8 @@ const StreamingStatus = memo(function StreamingStatus({
|
|||||||
|
|
||||||
const [shimmerOffset, setShimmerOffset] = useState(-3);
|
const [shimmerOffset, setShimmerOffset] = useState(-3);
|
||||||
const [elapsedMs, setElapsedMs] = useState(0);
|
const [elapsedMs, setElapsedMs] = useState(0);
|
||||||
|
const [tipMessage, setTipMessage] = useState("");
|
||||||
|
const tipInitializedRef = useRef(false);
|
||||||
const streamStartRef = useRef<number | null>(null);
|
const streamStartRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -578,6 +581,18 @@ const StreamingStatus = memo(function StreamingStatus({
|
|||||||
setElapsedMs(0);
|
setElapsedMs(0);
|
||||||
}, [streaming, visible]);
|
}, [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 estimatedTokens = charsToTokens(tokenCount);
|
||||||
const totalElapsedMs = elapsedBaseMs + elapsedMs;
|
const totalElapsedMs = elapsedBaseMs + elapsedMs;
|
||||||
const shouldShowTokenCount =
|
const shouldShowTokenCount =
|
||||||
@@ -650,33 +665,46 @@ const StreamingStatus = memo(function StreamingStatus({
|
|||||||
hintColor(" (") + hintBold("esc") + hintColor(` to interrupt${suffix}`)
|
hintColor(" (") + hintBold("esc") + hintColor(` to interrupt${suffix}`)
|
||||||
);
|
);
|
||||||
}, [interruptRequested, statusHintSuffix]);
|
}, [interruptRequested, statusHintSuffix]);
|
||||||
|
const tipLineText = useMemo(() => {
|
||||||
|
return truncateEnd(`⎿ Tip: ${tipMessage}`, statusContentWidth);
|
||||||
|
}, [tipMessage, statusContentWidth]);
|
||||||
|
|
||||||
if (!streaming || !visible) {
|
if (!streaming || !visible) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Box width={2} flexShrink={0}>
|
<Box flexDirection="row">
|
||||||
<Text color={colors.status.processing}>
|
<Box width={2} flexShrink={0}>
|
||||||
{animate ? <Spinner type="layer" /> : "●"}
|
<Text color={colors.status.processing}>
|
||||||
</Text>
|
{animate ? <Spinner type="layer" /> : "●"}
|
||||||
</Box>
|
</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>
|
||||||
{hintColumnWidth > 0 && (
|
<Box width={statusContentWidth} flexShrink={0} flexDirection="row">
|
||||||
<Box width={hintColumnWidth} flexShrink={0}>
|
<Box width={messageColumnWidth} flexShrink={0}>
|
||||||
<Text wrap="truncate-end">{statusHintText}</Text>
|
<ShimmerText
|
||||||
|
boldPrefix={agentName || undefined}
|
||||||
|
message={thinkingMessage}
|
||||||
|
shimmerOffset={animate ? shimmerOffset : -3}
|
||||||
|
wrap="truncate-end"
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{hintColumnWidth > 0 && (
|
||||||
<Box flexGrow={1} />
|
<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>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ const THINKING_VERBS = [
|
|||||||
"internalizing",
|
"internalizing",
|
||||||
] as const;
|
] 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];
|
type ThinkingVerb = (typeof THINKING_VERBS)[number];
|
||||||
|
|
||||||
const PAST_TENSE_VERBS: Record<ThinkingVerb, string> = {
|
const PAST_TENSE_VERBS: Record<ThinkingVerb, string> = {
|
||||||
@@ -95,6 +103,11 @@ function getRandomVerb(): string {
|
|||||||
return THINKING_VERBS[index] ?? "thinking";
|
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")
|
// Get a random thinking verb phrase (e.g., "is thinking", "is processing")
|
||||||
export function getRandomThinkingVerb(): string {
|
export function getRandomThinkingVerb(): string {
|
||||||
return `is ${getRandomVerb()}`;
|
return `is ${getRandomVerb()}`;
|
||||||
@@ -117,3 +130,7 @@ export function getRandomThinkingMessage(agentName?: string | null): string {
|
|||||||
// Fallback to capitalized verb if no agent name
|
// Fallback to capitalized verb if no agent name
|
||||||
return verb.charAt(0).toUpperCase() + verb.slice(1);
|
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 { describe, expect, test } from "bun:test";
|
||||||
import { getRandomThinkingMessage } from "../../cli/helpers/thinkingMessages";
|
import {
|
||||||
|
getRandomThinkingMessage,
|
||||||
|
getRandomThinkingTip,
|
||||||
|
THINKING_TIPS,
|
||||||
|
} from "../../cli/helpers/thinkingMessages";
|
||||||
|
|
||||||
describe("Thinking messages", () => {
|
describe("Thinking messages", () => {
|
||||||
test("returns formatted message with agent name", () => {
|
test("returns formatted message with agent name", () => {
|
||||||
@@ -43,4 +47,11 @@ describe("Thinking messages", () => {
|
|||||||
// Should have more than 1 unique message (with high probability)
|
// Should have more than 1 unique message (with high probability)
|
||||||
expect(messages.size).toBeGreaterThan(1);
|
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