diff --git a/src/cli/App.tsx b/src/cli/App.tsx index e786646..4cafc03 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -142,6 +142,7 @@ import { ApprovalSwitch } from "./components/ApprovalSwitch"; import { AssistantMessage } from "./components/AssistantMessageRich"; import { BashCommandMessage } from "./components/BashCommandMessage"; import { CommandMessage } from "./components/CommandMessage"; +import { CompactionSelector } from "./components/CompactionSelector"; import { ConversationSelector } from "./components/ConversationSelector"; import { colors } from "./components/colors"; // EnterPlanModeDialog removed - now using InlineEnterPlanModeApproval @@ -1352,6 +1353,7 @@ export default function App({ type ActiveOverlay = | "model" | "sleeptime" + | "compaction" | "toolset" | "system" | "agent" @@ -1423,6 +1425,11 @@ export default function App({ settings: ReflectionSettings; commandId?: string; } + | { + type: "set_compaction"; + mode: string; + commandId?: string; + } | { type: "switch_conversation"; conversationId: string; @@ -6808,6 +6815,18 @@ export default function App({ return { submitted: true }; } + // Special handling for /compaction command - opens compaction mode settings + if (trimmed === "/compaction") { + startOverlayCommand( + "compaction", + "/compaction", + "Opening compaction settings...", + "Compaction settings dismissed", + ); + setActiveOverlay("compaction"); + return { submitted: true }; + } + // Special handling for /toolset command - opens selector if (trimmed === "/toolset") { startOverlayCommand( @@ -11213,6 +11232,78 @@ ${SYSTEM_REMINDER_CLOSE} ], ); + const handleCompactionModeSelect = useCallback( + async (mode: string, commandId?: string | null) => { + const overlayCommand = commandId + ? commandRunner.getHandle(commandId, "/compaction") + : consumeOverlayCommand("compaction"); + + if (isAgentBusy()) { + setActiveOverlay(null); + const cmd = + overlayCommand ?? + commandRunner.start( + "/compaction", + "Compaction settings update queued – will apply after current task completes", + ); + cmd.update({ + output: + "Compaction settings update queued – will apply after current task completes", + phase: "running", + }); + setQueuedOverlayAction({ + type: "set_compaction", + mode, + commandId: cmd.id, + }); + return; + } + + await withCommandLock(async () => { + const cmd = + overlayCommand ?? + commandRunner.start("/compaction", "Saving compaction settings..."); + cmd.update({ + output: "Saving compaction settings...", + phase: "running", + }); + + try { + const client = await getClient(); + // Spread existing compaction_settings to preserve model/other fields, + // only override the mode. If no existing settings, use empty model + // string which tells the backend to use its default lightweight model. + const existing = agentState?.compaction_settings; + + await client.agents.update(agentId, { + compaction_settings: { + model: existing?.model ?? "", + ...existing, + mode: mode as + | "all" + | "sliding_window" + | "self_compact_all" + | "self_compact_sliding_window", + }, + }); + + cmd.finish(`Updated compaction mode to: ${mode}`, true); + } catch (error) { + const errorDetails = formatErrorDetails(error, agentId); + cmd.fail(`Failed to save compaction settings: ${errorDetails}`); + } + }); + }, + [ + agentId, + commandRunner, + consumeOverlayCommand, + isAgentBusy, + withCommandLock, + agentState?.compaction_settings, + ], + ); + const handleToolsetSelect = useCallback( async (toolsetId: ToolsetPreference, commandId?: string | null) => { const overlayCommand = commandId @@ -11346,6 +11437,8 @@ ${SYSTEM_REMINDER_CLOSE} handleModelSelect(action.modelId, action.commandId); } else if (action.type === "set_sleeptime") { handleSleeptimeModeSelect(action.settings, action.commandId); + } else if (action.type === "set_compaction") { + handleCompactionModeSelect(action.mode, action.commandId); } else if (action.type === "switch_conversation") { const cmd = action.commandId ? commandRunner.getHandle(action.commandId, "/resume") @@ -11426,6 +11519,7 @@ ${SYSTEM_REMINDER_CLOSE} handleAgentSelect, handleModelSelect, handleSleeptimeModeSelect, + handleCompactionModeSelect, handleToolsetSelect, handleSystemPromptSelect, agentId, @@ -12840,6 +12934,14 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa /> )} + {activeOverlay === "compaction" && ( + + )} + {/* GitHub App Installer - setup Letta Code GitHub Action */} {activeOverlay === "install-github-app" && ( = { return "Opening sleeptime settings..."; }, }, + "/compaction": { + desc: "Configure compaction mode settings", + order: 15.6, + noArgs: true, + handler: () => { + // Handled specially in App.tsx to open compaction settings + return "Opening compaction settings..."; + }, + }, "/memfs": { desc: "Manage filesystem-backed memory (/memfs [enable|disable|sync|reset])", args: "[enable|disable|sync|reset]", diff --git a/src/cli/components/CompactionSelector.tsx b/src/cli/components/CompactionSelector.tsx new file mode 100644 index 0000000..0ddb15b --- /dev/null +++ b/src/cli/components/CompactionSelector.tsx @@ -0,0 +1,128 @@ +import { Box, useInput } from "ink"; +import { useMemo, useState } from "react"; +import { useTerminalWidth } from "../hooks/useTerminalWidth"; +import { colors } from "./colors"; +import { Text } from "./Text"; + +const SOLID_LINE = "─"; + +type CompactionMode = + | "all" + | "sliding_window" + | "self_compact_all" + | "self_compact_sliding_window"; +const MODE_OPTIONS: CompactionMode[] = [ + "all", + "sliding_window", + "self_compact_all", + "self_compact_sliding_window", +]; +const MODE_LABELS: Record = { + all: "All", + sliding_window: "Sliding Window", + self_compact_all: "Self Compact All", + self_compact_sliding_window: "Self Compact Sliding Window", +}; + +function cycleOption( + options: readonly T[], + current: T, + direction: -1 | 1, +): T { + if (options.length === 0) { + return current; + } + const currentIndex = options.indexOf(current); + const safeIndex = currentIndex >= 0 ? currentIndex : 0; + const nextIndex = (safeIndex + direction + options.length) % options.length; + return options[nextIndex] ?? current; +} + +interface CompactionSelectorProps { + initialMode: string | null | undefined; + onSave: (mode: CompactionMode) => void; + onCancel: () => void; +} + +export function CompactionSelector({ + initialMode, + onSave, + onCancel, +}: CompactionSelectorProps) { + const terminalWidth = useTerminalWidth(); + const solidLine = SOLID_LINE.repeat(Math.max(terminalWidth, 10)); + + const parsedInitialMode = useMemo((): CompactionMode => { + if ( + initialMode === "all" || + initialMode === "sliding_window" || + initialMode === "self_compact_all" || + initialMode === "self_compact_sliding_window" + ) { + return initialMode as CompactionMode; + } + return "sliding_window"; + }, [initialMode]); + + const [mode, setMode] = useState(parsedInitialMode); + + useInput((input, key) => { + if (key.ctrl && input === "c") { + onCancel(); + return; + } + + if (key.escape) { + onCancel(); + return; + } + + if (key.return) { + onSave(mode); + return; + } + + if (key.leftArrow || key.rightArrow || key.tab) { + const direction: -1 | 1 = key.leftArrow ? -1 : 1; + setMode((prev) => cycleOption(MODE_OPTIONS, prev, direction)); + } + }); + + return ( + + {"> /compaction"} + {solidLine} + + + + + Configure compaction mode + + + + + + {"> "} + Mode: + {" "} + {MODE_OPTIONS.map((opt) => ( + + + {` ${MODE_LABELS[opt]} `} + + + + ))} + + + + {" Enter to save · ←→/Tab options · Esc cancel"} + + ); +}