From 3f189ed0c8a66fe24006799eee85a662c9e7f689 Mon Sep 17 00:00:00 2001
From: amysguan <64990783+amysguan@users.noreply.github.com>
Date: Mon, 2 Mar 2026 11:28:37 -0800
Subject: [PATCH] feat(tui): add /compaction mode selector command (#1141)
---
src/cli/App.tsx | 102 +++++++++++++++++
src/cli/commands/registry.ts | 9 ++
src/cli/components/CompactionSelector.tsx | 128 ++++++++++++++++++++++
3 files changed, 239 insertions(+)
create mode 100644 src/cli/components/CompactionSelector.tsx
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"}
+
+ );
+}