feat: add /ade command to open agent in browser (#409)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
123
src/cli/App.tsx
123
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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user