feat(tui): add /compaction mode selector command (#1141)

This commit is contained in:
amysguan
2026-03-02 11:28:37 -08:00
committed by GitHub
parent 04dcff9c33
commit 3f189ed0c8
3 changed files with 239 additions and 0 deletions

View File

@@ -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" && (
<CompactionSelector
initialMode={agentState?.compaction_settings?.mode}
onSave={handleCompactionModeSelect}
onCancel={closeOverlay}
/>
)}
{/* GitHub App Installer - setup Letta Code GitHub Action */}
{activeOverlay === "install-github-app" && (
<InstallGithubAppFlow

View File

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

View File

@@ -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<CompactionMode, string> = {
all: "All",
sliding_window: "Sliding Window",
self_compact_all: "Self Compact All",
self_compact_sliding_window: "Self Compact Sliding Window",
};
function cycleOption<T extends string>(
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<CompactionMode>(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 (
<Box flexDirection="column">
<Text dimColor>{"> /compaction"}</Text>
<Text dimColor>{solidLine}</Text>
<Box height={1} />
<Text bold color={colors.selector.title}>
Configure compaction mode
</Text>
<Box height={1} />
<Box flexDirection="row">
<Text>{"> "}</Text>
<Text bold>Mode:</Text>
<Text>{" "}</Text>
{MODE_OPTIONS.map((opt) => (
<Box key={opt} flexDirection="row">
<Text
backgroundColor={
mode === opt ? colors.selector.itemHighlighted : undefined
}
color={mode === opt ? "black" : undefined}
bold={mode === opt}
>
{` ${MODE_LABELS[opt]} `}
</Text>
<Text> </Text>
</Box>
))}
</Box>
<Box height={1} />
<Text dimColor>{" Enter to save · ←→/Tab options · Esc cancel"}</Text>
</Box>
);
}