From b1343d92ae782d624472e0ed4d10e088db483612 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Sun, 28 Dec 2025 22:03:56 -0800 Subject: [PATCH] feat: add /ade command to open agent in browser (#409) Co-authored-by: Letta --- src/cli/App.tsx | 123 ++++++++++++++++++++------- src/cli/commands/registry.ts | 8 ++ src/cli/components/StatusMessage.tsx | 33 ++++++- src/cli/components/WelcomeScreen.tsx | 33 ------- 4 files changed, 131 insertions(+), 66 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 20cc230..5f3c6f4 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -85,7 +85,7 @@ import { SystemPromptSelector } from "./components/SystemPromptSelector"; import { ToolCallMessage } from "./components/ToolCallMessageRich"; import { ToolsetSelector } from "./components/ToolsetSelector"; import { UserMessage } from "./components/UserMessageRich"; -import { getAgentStatusHints, WelcomeScreen } from "./components/WelcomeScreen"; +import { WelcomeScreen } from "./components/WelcomeScreen"; import { type Buffers, createBuffers, @@ -843,15 +843,35 @@ export default function App({ const shortCwd = cwd.startsWith(process.env.HOME || "") ? `~${cwd.slice((process.env.HOME || "").length)}` : cwd; - const agentUrl = agentState?.id - ? `https://app.letta.com/agents/${agentState.id}` - : null; - const statusLines = [ - `Connecting to last used agent in ${shortCwd}`, - agentState?.name ? `→ Agent: ${agentState.name}` : "", - agentUrl ? `→ ${agentUrl}` : "", - "→ Use /pinned or /agents to switch agents", - ].filter(Boolean); + + // Check if agent is pinned (locally or globally) + const isPinned = agentState?.id + ? settingsManager.getLocalPinnedAgents().includes(agentState.id) || + settingsManager.getGlobalPinnedAgents().includes(agentState.id) + : false; + + // Build status message + const agentName = agentState?.name || "Unnamed Agent"; + const headerMessage = `Connecting to **${agentName}** (last used in ${shortCwd})`; + + // Command hints - for pinned agents show /memory, for unpinned show /pin + const commandHints = isPinned + ? [ + "→ **/memory** view your agent's memory blocks", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + "→ **/agents** list agents", + "→ **/ade** open in the browser (web UI)", + ] + : [ + "→ **/pin** save + name your agent", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + "→ **/agents** list agents", + "→ **/ade** open in the browser (web UI)", + ]; + + const statusLines = [headerMessage, ...commandHints]; buffersRef.current.byId.set(statusId, { kind: "status", id: statusId, @@ -2268,6 +2288,32 @@ export default function App({ return { submitted: true }; } + // Special handling for /ade command - open agent in browser + if (trimmed === "/ade") { + const adeUrl = `https://app.letta.com/agents/${agentId}`; + const cmdId = uid("cmd"); + + // Fire-and-forget browser open + import("open") + .then(({ default: open }) => open(adeUrl, { wait: false })) + .catch(() => { + // Silently ignore - user can use the URL from the output + }); + + // Always show the URL in case browser doesn't open + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: "/ade", + output: `Opening ADE...\n→ ${adeUrl}`, + phase: "finished", + success: true, + }); + buffersRef.current.order.push(cmdId); + refreshDerived(); + return { submitted: true }; + } + // Special handling for /system command - opens system prompt selector if (trimmed === "/system") { setActiveOverlay("system"); @@ -5216,31 +5262,44 @@ Plan file path: ${planFilePath}`; ]); // Add status line showing agent info - const agentUrl = agentState?.id - ? `https://app.letta.com/agents/${agentState.id}` - : null; const statusId = `status-agent-${Date.now().toString(36)}`; - const hints = getAgentStatusHints( - !!continueSession, - agentState, - agentProvenance, - ); - // For resumed agents, show the agent name if it has one (profile name) - const resumedMessage = continueSession - ? agentState?.name - ? `Resumed **${agentState.name}**` - : "Resumed agent" - : "Creating a new agent (use /pin to save)"; - const statusLines = continueSession - ? [resumedMessage, ...hints, agentUrl ? `→ ${agentUrl}` : ""].filter( - Boolean, - ) + // Get short path for display + const cwd = process.cwd(); + const shortCwd = cwd.startsWith(process.env.HOME || "") + ? `~${cwd.slice((process.env.HOME || "").length)}` + : cwd; + + // Check if agent is pinned (locally or globally) + const isPinned = agentState?.id + ? settingsManager.getLocalPinnedAgents().includes(agentState.id) || + settingsManager.getGlobalPinnedAgents().includes(agentState.id) + : false; + + // Build status message based on session type + const agentName = agentState?.name || "Unnamed Agent"; + const headerMessage = continueSession + ? `Connecting to **${agentName}** (last used in ${shortCwd})` + : "Creating a new agent"; + + // Command hints - for pinned agents show /memory, for unpinned show /pin + const commandHints = isPinned + ? [ + "→ **/memory** view your agent's memory blocks", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + "→ **/agents** list agents", + "→ **/ade** open in the browser (web UI)", + ] : [ - resumedMessage, - agentUrl ? `→ ${agentUrl}` : "", - "→ Tip: use /init to initialize your agent's memory system!", - ].filter(Boolean); + "→ **/pin** save + name your agent", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + "→ **/agents** list agents", + "→ **/ade** open in the browser (web UI)", + ]; + + const statusLines = [headerMessage, ...commandHints]; buffersRef.current.byId.set(statusId, { kind: "status", diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 8582649..1be79ad 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -141,6 +141,14 @@ export const commands: Record = { return "Opening toolset selector..."; }, }, + "/ade": { + desc: "Open agent in ADE (browser)", + order: 28, + handler: () => { + // Handled specially in App.tsx to access agent ID and open browser + return "Opening ADE..."; + }, + }, // === Page 3: Advanced features (order 30-39) === "/system": { diff --git a/src/cli/components/StatusMessage.tsx b/src/cli/components/StatusMessage.tsx index 9011dad..dce4c80 100644 --- a/src/cli/components/StatusMessage.tsx +++ b/src/cli/components/StatusMessage.tsx @@ -1,6 +1,7 @@ import { Box, Text } from "ink"; import { memo } from "react"; import { useTerminalWidth } from "../hooks/useTerminalWidth"; +import { colors } from "./colors"; type StatusLine = { kind: "status"; @@ -8,6 +9,35 @@ type StatusLine = { lines: string[]; }; +/** + * Parse text with **highlighted** segments and render with colors. + * Text wrapped in ** will be rendered with the accent color. + */ +function renderColoredText(text: string): React.ReactNode { + // Split on **...** pattern, keeping the delimiters + const parts = text.split(/(\*\*[^*]+\*\*)/g); + + return parts.map((part, i) => { + if (part.startsWith("**") && part.endsWith("**")) { + // Remove ** markers and render with accent color + const content = part.slice(2, -2); + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: Static text parts never reorder + + {content} + + ); + } + // Regular dimmed text + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: Static text parts never reorder + + {part} + + ); + }); +} + /** * StatusMessage - Displays multi-line status messages * @@ -16,6 +46,7 @@ type StatusLine = { * - Where memory blocks came from (global/project/new) * * Layout matches ErrorMessage with a left column icon (grey circle) + * Supports **text** syntax for highlighted (accent colored) text. */ export const StatusMessage = memo(({ line }: { line: StatusLine }) => { const columns = useTerminalWidth(); @@ -30,7 +61,7 @@ export const StatusMessage = memo(({ line }: { line: StatusLine }) => { {idx === 0 ? "●" : " "} - {text} + {renderColoredText(text)} ))} diff --git a/src/cli/components/WelcomeScreen.tsx b/src/cli/components/WelcomeScreen.tsx index 2022e94..57c9bd3 100644 --- a/src/cli/components/WelcomeScreen.tsx +++ b/src/cli/components/WelcomeScreen.tsx @@ -55,39 +55,6 @@ type LoadingState = | "selecting_global" | "ready"; -/** - * Generate status hints based on session type and block provenance. - * Pure function - no React dependencies. - */ -export function getAgentStatusHints( - continueSession: boolean, - agentState?: Letta.AgentState | null, - _agentProvenance?: AgentProvenance | null, -): string[] { - const hints: string[] = []; - - // For resumed agents, show memory blocks and --new hint - if (continueSession) { - if (agentState?.memory?.blocks) { - const blocks = agentState.memory.blocks; - const count = blocks.length; - const labels = blocks - .map((b) => b.label) - .filter(Boolean) - .join(", "); - if (labels) { - hints.push( - `→ Attached ${count} memory block${count !== 1 ? "s" : ""}: ${labels}`, - ); - } - } - hints.push("→ To create a new agent, use --new"); - return hints; - } - - return hints; -} - export function WelcomeScreen({ loadingState, continueSession,