feat(tui): add /compaction mode selector command (#1141)
This commit is contained in:
102
src/cli/App.tsx
102
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" && (
|
||||
<CompactionSelector
|
||||
initialMode={agentState?.compaction_settings?.mode}
|
||||
onSave={handleCompactionModeSelect}
|
||||
onCancel={closeOverlay}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* GitHub App Installer - setup Letta Code GitHub Action */}
|
||||
{activeOverlay === "install-github-app" && (
|
||||
<InstallGithubAppFlow
|
||||
|
||||
@@ -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]",
|
||||
|
||||
128
src/cli/components/CompactionSelector.tsx
Normal file
128
src/cli/components/CompactionSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user