feat: add /ade command to open agent in browser (#409)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-28 22:03:56 -08:00
committed by GitHub
parent 0b8fcf563d
commit b1343d92ae
4 changed files with 131 additions and 66 deletions

View File

@@ -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",

View File

@@ -141,6 +141,14 @@ export const commands: Record<string, Command> = {
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": {

View File

@@ -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
<Text key={i} color={colors.footer.agentName}>
{content}
</Text>
);
}
// Regular dimmed text
return (
// biome-ignore lint/suspicious/noArrayIndexKey: Static text parts never reorder
<Text key={i} dimColor>
{part}
</Text>
);
});
}
/**
* 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 }) => {
<Text dimColor>{idx === 0 ? "●" : " "}</Text>
</Box>
<Box flexGrow={1} width={contentWidth}>
<Text dimColor>{text}</Text>
<Text>{renderColoredText(text)}</Text>
</Box>
</Box>
))}

View File

@@ -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,