diff --git a/README.md b/README.md index 82fe0d9..28f091c 100644 --- a/README.md +++ b/README.md @@ -169,9 +169,13 @@ While in a session, you can use these commands: **Agent Management:** - `/agent` - Show current agent link -- `/swap` - Switch to a different agent (prompts with agent selector) +- `/resume` - Switch to a different agent (prompts with agent selector) - `/rename` - Rename the current agent - `/download` - Download agent file locally (exports agent configuration as JSON) +- `/profile` - List saved profiles +- `/profile save ` - Save current agent to a named profile +- `/profile load ` - Load a saved profile (switches to that agent) +- `/profile delete ` - Delete a saved profile **Configuration:** - `/model` - Switch models (prompts with model selector) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 1814c2e..597e950 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -11,7 +11,7 @@ import type { Message, } from "@letta-ai/letta-client/resources/agents/messages"; import type { LlmConfig } from "@letta-ai/letta-client/resources/models/models"; -import { Box, Static } from "ink"; +import { Box, Static, Text } from "ink"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ApprovalResult } from "../agent/approval-execution"; import { getResumeData } from "../agent/check-approval"; @@ -30,6 +30,15 @@ import { executeTool, savePermissionRule, } from "../tools/manager"; +import { + addCommandResult, + handleProfileDelete, + handleProfileList, + handleProfileSave, + handleProfileUsage, + type ProfileCommandContext, + validateProfileLoad, +} from "./commands/profile"; import { AgentSelector } from "./components/AgentSelector"; import { ApprovalDialog } from "./components/ApprovalDialogRich"; import { AssistantMessage } from "./components/AssistantMessageRich"; @@ -244,20 +253,34 @@ export default function App({ const [agentId, setAgentId] = useState(initialAgentId); const [agentState, setAgentState] = useState(initialAgentState); + // Keep a ref to the current agentId for use in callbacks that need the latest value + const agentIdRef = useRef(agentId); + useEffect(() => { + agentIdRef.current = agentId; + }, [agentId]); + const resumeKey = useSuspend(); + // Track previous prop values to detect actual prop changes (not internal state changes) + const prevInitialAgentIdRef = useRef(initialAgentId); + const prevInitialAgentStateRef = useRef(initialAgentState); + // Sync with prop changes (e.g., when parent updates from "loading" to actual ID) + // Only sync when the PROP actually changes, not when internal state changes useEffect(() => { - if (initialAgentId !== agentId) { + if (initialAgentId !== prevInitialAgentIdRef.current) { + prevInitialAgentIdRef.current = initialAgentId; + agentIdRef.current = initialAgentId; setAgentId(initialAgentId); } - }, [initialAgentId, agentId]); + }, [initialAgentId]); useEffect(() => { - if (initialAgentState !== agentState) { + if (initialAgentState !== prevInitialAgentStateRef.current) { + prevInitialAgentStateRef.current = initialAgentState; setAgentState(initialAgentState); } - }, [initialAgentState, agentState]); + }, [initialAgentState]); // Whether a stream is in flight (disables input) const [streaming, setStreaming] = useState(false); @@ -268,6 +291,13 @@ export default function App({ // Whether a command is running (disables input but no streaming UI) const [commandRunning, setCommandRunning] = useState(false); + // Profile load confirmation - when loading a profile and current agent is unsaved + const [profileConfirmPending, setProfileConfirmPending] = useState<{ + name: string; + agentId: string; + cmdId: string; + } | null>(null); + // If we have approval requests, we should show the approval dialog instead of the input area const [pendingApprovals, setPendingApprovals] = useState( [], @@ -630,8 +660,11 @@ export default function App({ return; } - // Stream one turn - const stream = await sendMessageStream(agentId, currentInput); + // Stream one turn - use ref to always get the latest agentId + const stream = await sendMessageStream( + agentIdRef.current, + currentInput, + ); const { stopReason, approval, approvals, apiDurationMs, lastRunId } = await drainStreamWithResume( stream, @@ -1072,7 +1105,10 @@ export default function App({ run_id: lastRunId, }, }; - const errorDetails = formatErrorDetails(errorObject, agentId); + const errorDetails = formatErrorDetails( + errorObject, + agentIdRef.current, + ); appendError(errorDetails); } else { // No error metadata, show generic error with run info @@ -1111,7 +1147,7 @@ export default function App({ } // Use comprehensive error formatting - const errorDetails = formatErrorDetails(e, agentId); + const errorDetails = formatErrorDetails(e, agentIdRef.current); appendError(errorDetails); setStreaming(false); refreshDerived(); @@ -1119,7 +1155,7 @@ export default function App({ abortControllerRef.current = null; } }, - [agentId, appendError, refreshDerived, refreshDerivedThrottled], + [appendError, refreshDerived, refreshDerivedThrottled], ); const handleExit = useCallback(() => { @@ -1223,9 +1259,134 @@ export default function App({ } }, [streaming]); + const handleAgentSelect = useCallback( + async (targetAgentId: string, opts?: { profileName?: string }) => { + setAgentSelectorOpen(false); + + const isProfileLoad = !!opts?.profileName; + const inputCmd = isProfileLoad + ? `/profile load ${opts.profileName}` + : `/resume ${targetAgentId}`; + + setCommandRunning(true); + + try { + const client = await getClient(); + // Fetch new agent + const agent = await client.agents.retrieve(targetAgentId); + + // Fetch agent's message history + const messagesPage = await client.agents.messages.list(targetAgentId); + const messages = messagesPage.items; + + // Update project settings with new agent + await updateProjectSettings({ lastAgent: targetAgentId }); + + // Clear current transcript and static items + buffersRef.current.byId.clear(); + buffersRef.current.order = []; + buffersRef.current.tokenCount = 0; + emittedIdsRef.current.clear(); + setStaticItems([]); + setStaticRenderEpoch((e) => e + 1); + + // Update agent state - also update ref immediately for any code that runs before re-render + agentIdRef.current = targetAgentId; + setAgentId(targetAgentId); + setAgentState(agent); + setAgentName(agent.name); + setLlmConfig(agent.llm_config); + + // Build success command + const agentUrl = `https://app.letta.com/projects/default-project/agents/${targetAgentId}`; + const successOutput = isProfileLoad + ? `Loaded "${agent.name || targetAgentId}"\n⎿ ${agentUrl}` + : `Resumed "${agent.name || targetAgentId}"\n⎿ ${agentUrl}`; + const successItem: StaticItem = { + kind: "command", + id: uid("cmd"), + input: inputCmd, + output: successOutput, + phase: "finished", + success: true, + }; + + // Backfill message history with visual separator, then success command at end + if (messages.length > 0) { + hasBackfilledRef.current = false; + backfillBuffers(buffersRef.current, messages); + // Collect backfilled items + const backfilledItems: StaticItem[] = []; + for (const id of buffersRef.current.order) { + const ln = buffersRef.current.byId.get(id); + if (!ln) continue; + emittedIdsRef.current.add(id); + backfilledItems.push({ ...ln } as StaticItem); + } + // Add separator before backfilled messages, then success at end + const separator = { + kind: "separator" as const, + id: uid("sep"), + }; + setStaticItems([separator, ...backfilledItems, successItem]); + setLines(toLines(buffersRef.current)); + hasBackfilledRef.current = true; + } else { + setStaticItems([successItem]); + } + } catch (error) { + const errorDetails = formatErrorDetails(error, agentId); + const errorCmdId = uid("cmd"); + buffersRef.current.byId.set(errorCmdId, { + kind: "command", + id: errorCmdId, + input: inputCmd, + output: `Failed: ${errorDetails}`, + phase: "finished", + success: false, + }); + buffersRef.current.order.push(errorCmdId); + refreshDerived(); + } finally { + setCommandRunning(false); + } + }, + [refreshDerived, agentId], + ); + const onSubmit = useCallback( async (message?: string): Promise<{ submitted: boolean }> => { const msg = message?.trim() ?? ""; + + // Handle profile load confirmation (Enter to continue) + if (profileConfirmPending && !msg) { + // User pressed Enter with empty input - proceed with loading + const { name, agentId: targetAgentId, cmdId } = profileConfirmPending; + buffersRef.current.byId.delete(cmdId); + const orderIdx = buffersRef.current.order.indexOf(cmdId); + if (orderIdx !== -1) buffersRef.current.order.splice(orderIdx, 1); + refreshDerived(); + setProfileConfirmPending(null); + await handleAgentSelect(targetAgentId, { profileName: name }); + return { submitted: true }; + } + + // Cancel profile confirmation if user types something else + if (profileConfirmPending && msg) { + const { cmdId } = profileConfirmPending; + buffersRef.current.byId.set(cmdId, { + kind: "command", + id: cmdId, + input: `/profile load ${profileConfirmPending.name}`, + output: "Cancelled", + phase: "finished", + success: false, + }); + refreshDerived(); + setProfileConfirmPending(null); + // Continue processing the new message + } + if (!msg) return { submitted: false }; // Block submission if waiting for explicit user action (approvals) @@ -1703,6 +1864,81 @@ export default function App({ return { submitted: true }; } + // Special handling for /profile command - manage local profiles + if (msg.trim().startsWith("/profile")) { + const parts = msg.trim().split(/\s+/); + const subcommand = parts[1]?.toLowerCase(); + const profileName = parts.slice(2).join(" "); + + const profileCtx: ProfileCommandContext = { + buffersRef, + refreshDerived, + agentId, + setCommandRunning, + setAgentName, + }; + + // /profile - list all profiles + if (!subcommand) { + handleProfileList(profileCtx, msg); + return { submitted: true }; + } + + // /profile save + if (subcommand === "save") { + await handleProfileSave(profileCtx, msg, profileName); + return { submitted: true }; + } + + // /profile load + if (subcommand === "load") { + const validation = validateProfileLoad( + profileCtx, + msg, + profileName, + ); + if (validation.errorMessage) { + return { submitted: true }; + } + + if (validation.needsConfirmation && validation.targetAgentId) { + // Show warning and wait for confirmation + const cmdId = addCommandResult( + buffersRef, + refreshDerived, + msg, + "Warning: Current agent is not saved to any profile.\nPress Enter to continue, or type anything to cancel.", + false, + "running", + ); + setProfileConfirmPending({ + name: profileName, + agentId: validation.targetAgentId, + cmdId, + }); + return { submitted: true }; + } + + // Current agent is saved, proceed with loading + if (validation.targetAgentId) { + await handleAgentSelect(validation.targetAgentId, { + profileName, + }); + } + return { submitted: true }; + } + + // /profile delete + if (subcommand === "delete") { + handleProfileDelete(profileCtx, msg, profileName); + return { submitted: true }; + } + + // Unknown subcommand + handleProfileUsage(profileCtx, msg); + return { submitted: true }; + } + // Special handling for /bashes command - show background shell processes if (msg.trim() === "/bashes") { const { backgroundProcesses } = await import( @@ -2250,6 +2486,8 @@ ${recentCommits} isExecutingTool, queuedApprovalResults, pendingApprovals, + profileConfirmPending, + handleAgentSelect, tokenStreamingEnabled, ], ); @@ -2876,100 +3114,22 @@ ${recentCommits} [agentId, refreshDerived], ); - const handleAgentSelect = useCallback( - async (targetAgentId: string) => { - setAgentSelectorOpen(false); - - const cmdId = uid("cmd"); + // Handle escape when profile confirmation is pending + const handleProfileEscapeCancel = useCallback(() => { + if (profileConfirmPending) { + const { cmdId, name } = profileConfirmPending; buffersRef.current.byId.set(cmdId, { kind: "command", id: cmdId, - input: `/resume ${targetAgentId}`, - output: `Switching to agent ${targetAgentId}...`, - phase: "running", + input: `/profile load ${name}`, + output: "Cancelled", + phase: "finished", + success: false, }); - buffersRef.current.order.push(cmdId); refreshDerived(); - - setCommandRunning(true); - - try { - const client = await getClient(); - // Fetch new agent - const agent = await client.agents.retrieve(targetAgentId); - - // Fetch agent's message history - const messagesPage = await client.agents.messages.list(targetAgentId); - const messages = messagesPage.items; - - // Update project settings with new agent - await updateProjectSettings({ lastAgent: targetAgentId }); - - // Clear current transcript - buffersRef.current.byId.clear(); - buffersRef.current.order = []; - buffersRef.current.tokenCount = 0; - emittedIdsRef.current.clear(); - setStaticItems([]); - - // Update agent state - setAgentId(targetAgentId); - setAgentState(agent); - setAgentName(agent.name); - setLlmConfig(agent.llm_config); - - // Add welcome screen for new agent - welcomeCommittedRef.current = false; - setStaticItems([ - { - kind: "welcome", - id: `welcome-${Date.now().toString(36)}`, - snapshot: { - continueSession: true, - agentState: agent, - terminalWidth: columns, - }, - }, - ]); - - // Backfill message history - if (messages.length > 0) { - hasBackfilledRef.current = false; - backfillBuffers(buffersRef.current, messages); - refreshDerived(); - commitEligibleLines(buffersRef.current); - hasBackfilledRef.current = true; - } - - // Add success command to transcript - const successCmdId = uid("cmd"); - buffersRef.current.byId.set(successCmdId, { - kind: "command", - id: successCmdId, - input: `/resume ${targetAgentId}`, - output: `✓ Switched to agent "${agent.name || targetAgentId}"`, - phase: "finished", - success: true, - }); - buffersRef.current.order.push(successCmdId); - refreshDerived(); - } catch (error) { - const errorDetails = formatErrorDetails(error, agentId); - buffersRef.current.byId.set(cmdId, { - kind: "command", - id: cmdId, - input: `/resume ${targetAgentId}`, - output: `Failed: ${errorDetails}`, - phase: "finished", - success: false, - }); - refreshDerived(); - } finally { - setCommandRunning(false); - } - }, - [refreshDerived, commitEligibleLines, columns, agentId], - ); + setProfileConfirmPending(null); + } + }, [profileConfirmPending, refreshDerived]); // Track permission mode changes for UI updates const [uiPermissionMode, setUiPermissionMode] = useState( @@ -3287,6 +3447,8 @@ Plan file path: ${planFilePath}`; ) : item.kind === "status" ? ( + ) : item.kind === "separator" ? ( + {"─".repeat(columns)} ) : item.kind === "command" ? ( ) : null} @@ -3370,6 +3532,9 @@ Plan file path: ${planFilePath}`; currentModel={currentModelDisplay} messageQueue={messageQueue} onEnterQueueEditMode={handleEnterQueueEditMode} + onEscapeCancel={ + profileConfirmPending ? handleProfileEscapeCancel : undefined + } /> {/* Model Selector - conditionally mounted as overlay */} @@ -3417,9 +3582,9 @@ Plan file path: ${planFilePath}`; {resumeSelectorOpen && ( { + onSelect={async (id) => { setResumeSelectorOpen(false); - handleAgentSelect(id); + await handleAgentSelect(id); }} onCancel={() => setResumeSelectorOpen(false)} /> diff --git a/src/cli/commands/profile.ts b/src/cli/commands/profile.ts new file mode 100644 index 0000000..ffbe01d --- /dev/null +++ b/src/cli/commands/profile.ts @@ -0,0 +1,296 @@ +// src/cli/commands/profile.ts +// Profile command handlers for managing local agent profiles + +import { getClient } from "../../agent/client"; +import { settingsManager } from "../../settings-manager"; +import type { Buffers, Line } from "../helpers/accumulator"; +import { formatErrorDetails } from "../helpers/errorFormatter"; + +// tiny helper for unique ids +function uid(prefix: string) { + return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; +} + +// Helper type for command result +type CommandLine = Extract; + +// Context passed to profile handlers +export interface ProfileCommandContext { + buffersRef: { current: Buffers }; + refreshDerived: () => void; + agentId: string; + setCommandRunning: (running: boolean) => void; + setAgentName: (name: string) => void; +} + +// Helper to add a command result to buffers +export function addCommandResult( + buffersRef: { current: Buffers }, + refreshDerived: () => void, + input: string, + output: string, + success: boolean, + phase: "running" | "finished" = "finished", +): string { + const cmdId = uid("cmd"); + const line: CommandLine = { + kind: "command", + id: cmdId, + input, + output, + phase, + ...(phase === "finished" && { success }), + }; + buffersRef.current.byId.set(cmdId, line); + buffersRef.current.order.push(cmdId); + refreshDerived(); + return cmdId; +} + +// Helper to update an existing command result +export function updateCommandResult( + buffersRef: { current: Buffers }, + refreshDerived: () => void, + cmdId: string, + input: string, + output: string, + success: boolean, + phase: "running" | "finished" = "finished", +): void { + const line: CommandLine = { + kind: "command", + id: cmdId, + input, + output, + phase, + ...(phase === "finished" && { success }), + }; + buffersRef.current.byId.set(cmdId, line); + refreshDerived(); +} + +// Get profiles from local settings +export function getProfiles(): Record { + const localSettings = settingsManager.getLocalProjectSettings(); + return localSettings.profiles || {}; +} + +// Check if a profile exists, returns error message if not found +export function validateProfileExists( + profileName: string, + profiles: Record, +): string | null { + if (!profiles[profileName]) { + return `Profile "${profileName}" not found. Use /profile to list available profiles.`; + } + return null; +} + +// Check if a profile name was provided, returns error message if not +export function validateProfileNameProvided( + profileName: string, + action: string, +): string | null { + if (!profileName) { + return `Please provide a profile name: /profile ${action} `; + } + return null; +} + +// /profile - list all profiles +export function handleProfileList( + ctx: ProfileCommandContext, + msg: string, +): void { + const profiles = getProfiles(); + const profileNames = Object.keys(profiles); + + let output: string; + if (profileNames.length === 0) { + output = + "No profiles saved. Use /profile save to save the current agent."; + } else { + const lines = ["Saved profiles:"]; + for (const name of profileNames.sort()) { + const profileAgentId = profiles[name]; + const isCurrent = profileAgentId === ctx.agentId; + lines.push( + ` ${name} -> ${profileAgentId}${isCurrent ? " (current)" : ""}`, + ); + } + output = lines.join("\n"); + } + + addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, output, true); +} + +// /profile save +export async function handleProfileSave( + ctx: ProfileCommandContext, + msg: string, + profileName: string, +): Promise { + const validationError = validateProfileNameProvided(profileName, "save"); + if (validationError) { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + validationError, + false, + ); + return; + } + + const cmdId = addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + `Saving profile "${profileName}"...`, + false, + "running", + ); + + ctx.setCommandRunning(true); + + try { + const client = await getClient(); + // Update agent name via API + await client.agents.update(ctx.agentId, { name: profileName }); + ctx.setAgentName(profileName); + + // Save profile to local settings + const profiles = getProfiles(); + const updatedProfiles = { ...profiles, [profileName]: ctx.agentId }; + settingsManager.updateLocalProjectSettings({ + profiles: updatedProfiles, + }); + + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `Saved profile "${profileName}" (agent ${ctx.agentId})`, + true, + ); + } catch (error) { + const errorDetails = formatErrorDetails(error, ctx.agentId); + updateCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + cmdId, + msg, + `Failed: ${errorDetails}`, + false, + ); + } finally { + ctx.setCommandRunning(false); + } +} + +// Result from profile load validation +export interface ProfileLoadValidation { + targetAgentId: string | null; + needsConfirmation: boolean; + errorMessage: string | null; +} + +// /profile load - validation step (returns whether confirmation is needed) +export function validateProfileLoad( + ctx: ProfileCommandContext, + msg: string, + profileName: string, +): ProfileLoadValidation { + const nameError = validateProfileNameProvided(profileName, "load"); + if (nameError) { + addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, nameError, false); + return { + targetAgentId: null, + needsConfirmation: false, + errorMessage: nameError, + }; + } + + const profiles = getProfiles(); + const existsError = validateProfileExists(profileName, profiles); + if (existsError) { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + existsError, + false, + ); + return { + targetAgentId: null, + needsConfirmation: false, + errorMessage: existsError, + }; + } + + // We know the profile exists since validateProfileExists passed + const targetAgentId = profiles[profileName] as string; + + // Check if current agent is saved to any profile + const currentAgentSaved = Object.values(profiles).includes(ctx.agentId); + + if (!currentAgentSaved) { + return { targetAgentId, needsConfirmation: true, errorMessage: null }; + } + + return { targetAgentId, needsConfirmation: false, errorMessage: null }; +} + +// /profile delete +export function handleProfileDelete( + ctx: ProfileCommandContext, + msg: string, + profileName: string, +): void { + const nameError = validateProfileNameProvided(profileName, "delete"); + if (nameError) { + addCommandResult(ctx.buffersRef, ctx.refreshDerived, msg, nameError, false); + return; + } + + const profiles = getProfiles(); + const existsError = validateProfileExists(profileName, profiles); + if (existsError) { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + existsError, + false, + ); + return; + } + + const { [profileName]: _, ...remainingProfiles } = profiles; + settingsManager.updateLocalProjectSettings({ + profiles: remainingProfiles, + }); + + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + `Deleted profile "${profileName}"`, + true, + ); +} + +// Show usage help for unknown subcommand +export function handleProfileUsage( + ctx: ProfileCommandContext, + msg: string, +): void { + addCommandResult( + ctx.buffersRef, + ctx.refreshDerived, + msg, + "Usage: /profile [save|load|delete] \n /profile - list profiles\n /profile save - save current agent\n /profile load - load a profile\n /profile delete - delete a profile", + false, + ); +} diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 16e625a..01caf8f 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -135,6 +135,13 @@ export const commands: Record = { return "Opening message search..."; }, }, + "/profile": { + desc: "Manage local profiles (save/load/delete)", + handler: () => { + // Handled specially in App.tsx for profile management + return "Managing profiles..."; + }, + }, }; /** diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index f2c9974..33cd747 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -37,6 +37,7 @@ export function Input({ currentModel, messageQueue, onEnterQueueEditMode, + onEscapeCancel, }: { visible?: boolean; streaming: boolean; @@ -53,6 +54,7 @@ export function Input({ currentModel?: string | null; messageQueue?: string[]; onEnterQueueEditMode?: () => void; + onEscapeCancel?: () => void; }) { const [value, setValue] = useState(""); const [escapePressed, setEscapePressed] = useState(false); @@ -115,9 +117,28 @@ export function Input({ settings.env?.LETTA_BASE_URL || LETTA_CLOUD_API_URL; + // Handle profile confirmation: Enter confirms, any other key cancels + // When onEscapeCancel is provided, TextInput is unfocused so we handle all keys here + useInput((_input, key) => { + if (!visible) return; + if (!onEscapeCancel) return; + + // Enter key confirms the action - trigger submit with empty input + if (key.return) { + onSubmit(""); + return; + } + + // Any other key cancels + onEscapeCancel(); + }); + // Handle escape key for interrupt (when streaming) or double-escape-to-clear (when not) useInput((_input, key) => { if (!visible) return; + // Skip if onEscapeCancel is provided - handled by the confirmation handler above + if (onEscapeCancel) return; + if (key.escape) { // When streaming, use Esc to interrupt if (streaming && onInterrupt && !interruptRequested) { @@ -509,6 +530,7 @@ export function Input({ onSubmit={handleSubmit} cursorPosition={cursorPos} onCursorMove={setCurrentCursorPosition} + focus={!onEscapeCancel} /> diff --git a/src/cli/helpers/accumulator.ts b/src/cli/helpers/accumulator.ts index 34082dc..8fd5d10 100644 --- a/src/cli/helpers/accumulator.ts +++ b/src/cli/helpers/accumulator.ts @@ -49,7 +49,8 @@ export type Line = kind: "status"; id: string; lines: string[]; // Multi-line status message with arrow formatting - }; + } + | { kind: "separator"; id: string }; // Top-level state object for all streaming events export type Buffers = { diff --git a/src/settings-manager.ts b/src/settings-manager.ts index 38e35a5..71b3dd5 100644 --- a/src/settings-manager.ts +++ b/src/settings-manager.ts @@ -30,6 +30,7 @@ export interface ProjectSettings { export interface LocalProjectSettings { lastAgent: string | null; permissions?: PermissionRules; + profiles?: Record; // profileName -> agentId } const DEFAULT_SETTINGS: Settings = {