feat: system prompt swapping (#131)

This commit is contained in:
Charles Packer
2025-11-27 01:30:11 -08:00
committed by GitHub
parent 8330534f00
commit 135c19c7d7
10 changed files with 665 additions and 4 deletions

View File

@@ -43,6 +43,7 @@ import { PlanModeDialog } from "./components/PlanModeDialog";
// import { ReasoningMessage } from "./components/ReasoningMessage";
import { ReasoningMessage } from "./components/ReasoningMessageRich";
import { SessionStats as SessionStatsComponent } from "./components/SessionStats";
import { SystemPromptSelector } from "./components/SystemPromptSelector";
// import { ToolCallMessage } from "./components/ToolCallMessage";
import { ToolCallMessage } from "./components/ToolCallMessageRich";
import { ToolsetSelector } from "./components/ToolsetSelector";
@@ -216,6 +217,11 @@ export default function App({
// Model selector state
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [toolsetSelectorOpen, setToolsetSelectorOpen] = useState(false);
const [systemPromptSelectorOpen, setSystemPromptSelectorOpen] =
useState(false);
const [currentSystemPromptId, setCurrentSystemPromptId] = useState<
string | null
>("default");
const [currentToolset, setCurrentToolset] = useState<
"codex" | "default" | "gemini" | null
>(null);
@@ -816,6 +822,12 @@ export default function App({
return { submitted: true };
}
// Special handling for /system command - opens system prompt selector
if (msg.trim() === "/system") {
setSystemPromptSelectorOpen(true);
return { submitted: true };
}
// Special handling for /agent command - show agent link
if (msg.trim() === "/agent") {
const cmdId = uid("cmd");
@@ -1820,7 +1832,7 @@ export default function App({
// Update the same command with final result (include toolset info only if changed)
const autoToolsetLine = toolsetName
? `Automatically switched toolset to ${toolsetName}. Use /toolset to change back if desired.`
? `Automatically switched toolset to ${toolsetName}. Use /toolset to change back if desired.\nConsider switching to a different system prompt using /system to match.`
: null;
const outputLines = [
`Switched to ${selectedModel.label}`,
@@ -1857,6 +1869,90 @@ export default function App({
[agentId, refreshDerived, currentToolset],
);
const handleSystemPromptSelect = useCallback(
async (promptId: string) => {
setSystemPromptSelectorOpen(false);
const cmdId = uid("cmd");
try {
// Find the selected prompt
const { SYSTEM_PROMPTS } = await import("../agent/promptAssets");
const selectedPrompt = SYSTEM_PROMPTS.find((p) => p.id === promptId);
if (!selectedPrompt) {
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: `System prompt not found: ${promptId}`,
phase: "finished",
success: false,
});
buffersRef.current.order.push(cmdId);
refreshDerived();
return;
}
// Immediately add command to transcript with "running" phase
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: `Switching system prompt to ${selectedPrompt.label}...`,
phase: "running",
});
buffersRef.current.order.push(cmdId);
refreshDerived();
// Lock input during async operation
setCommandRunning(true);
// Update the agent's system prompt
const { updateAgentSystemPrompt } = await import("../agent/modify");
const result = await updateAgentSystemPrompt(
agentId,
selectedPrompt.content,
);
if (result.success) {
setCurrentSystemPromptId(promptId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: `Switched system prompt to ${selectedPrompt.label}`,
phase: "finished",
success: true,
});
} else {
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: result.message,
phase: "finished",
success: false,
});
}
refreshDerived();
} catch (error) {
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: `Failed to switch system prompt: ${error instanceof Error ? error.message : String(error)}`,
phase: "finished",
success: false,
});
refreshDerived();
} finally {
setCommandRunning(false);
}
},
[agentId, refreshDerived],
);
const handleToolsetSelect = useCallback(
async (toolsetId: "codex" | "default" | "gemini") => {
setToolsetSelectorOpen(false);
@@ -2229,6 +2325,7 @@ export default function App({
pendingApprovals.length === 0 &&
!modelSelectorOpen &&
!toolsetSelectorOpen &&
!systemPromptSelectorOpen &&
!agentSelectorOpen &&
!planApprovalPending
}
@@ -2264,6 +2361,15 @@ export default function App({
/>
)}
{/* System Prompt Selector - conditionally mounted as overlay */}
{systemPromptSelectorOpen && (
<SystemPromptSelector
currentPromptId={currentSystemPromptId ?? undefined}
onSelect={handleSystemPromptSelect}
onCancel={() => setSystemPromptSelectorOpen(false)}
/>
)}
{/* Agent Selector - conditionally mounted as overlay */}
{agentSelectorOpen && (
<AgentSelector

View File

@@ -85,6 +85,13 @@ export const commands: Record<string, Command> = {
return "Opening toolset selector...";
},
},
"/system": {
desc: "Switch system prompt",
handler: () => {
// Handled specially in App.tsx to open system prompt selector
return "Opening system prompt selector...";
},
},
"/download": {
desc: "Download agent file locally",
handler: () => {

View File

@@ -0,0 +1,111 @@
// Import useInput from vendored Ink for bracketed paste support
import { Box, Text, useInput } from "ink";
import { useMemo, useState } from "react";
import { SYSTEM_PROMPTS } from "../../agent/promptAssets";
import { colors } from "./colors";
interface SystemPromptSelectorProps {
currentPromptId?: string;
onSelect: (promptId: string) => void;
onCancel: () => void;
}
export function SystemPromptSelector({
currentPromptId,
onSelect,
onCancel,
}: SystemPromptSelectorProps) {
const [showAll, setShowAll] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const featuredPrompts = useMemo(
() => SYSTEM_PROMPTS.filter((prompt) => prompt.isFeatured),
[],
);
const visiblePrompts = useMemo(() => {
if (showAll) return SYSTEM_PROMPTS;
if (featuredPrompts.length > 0) return featuredPrompts;
return SYSTEM_PROMPTS.slice(0, 3);
}, [featuredPrompts, showAll]);
const hasHiddenPrompts = visiblePrompts.length < SYSTEM_PROMPTS.length;
const hasShowAllOption = !showAll && hasHiddenPrompts;
const totalItems = visiblePrompts.length + (hasShowAllOption ? 1 : 0);
useInput((_input, key) => {
if (key.upArrow) {
setSelectedIndex((prev) => Math.max(0, prev - 1));
} else if (key.downArrow) {
setSelectedIndex((prev) => Math.min(totalItems - 1, prev + 1));
} else if (key.return) {
if (hasShowAllOption && selectedIndex === visiblePrompts.length) {
setShowAll(true);
setSelectedIndex(0);
} else {
const selectedPrompt = visiblePrompts[selectedIndex];
if (selectedPrompt) {
onSelect(selectedPrompt.id);
}
}
} else if (key.escape) {
onCancel();
}
});
return (
<Box flexDirection="column" gap={1}>
<Box>
<Text bold color={colors.selector.title}>
Select System Prompt ( to navigate, Enter to select, ESC to cancel)
</Text>
</Box>
<Box flexDirection="column">
{visiblePrompts.map((prompt, index) => {
const isSelected = index === selectedIndex;
const isCurrent = prompt.id === currentPromptId;
return (
<Box key={prompt.id} flexDirection="row" gap={1}>
<Text
color={isSelected ? colors.selector.itemHighlighted : undefined}
>
{isSelected ? "" : " "}
</Text>
<Box flexDirection="row">
<Text
bold={isSelected}
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{prompt.label}
{isCurrent && (
<Text color={colors.selector.itemCurrent}> (current)</Text>
)}
</Text>
<Text dimColor> {prompt.description}</Text>
</Box>
</Box>
);
})}
{hasShowAllOption && (
<Box flexDirection="row" gap={1}>
<Text
color={
selectedIndex === visiblePrompts.length
? colors.selector.itemHighlighted
: undefined
}
>
{selectedIndex === visiblePrompts.length ? "" : " "}
</Text>
<Text dimColor>Show all prompts</Text>
</Box>
)}
</Box>
</Box>
);
}