feat: system prompt swapping (#131)
This commit is contained in:
108
src/cli/App.tsx
108
src/cli/App.tsx
@@ -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
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
111
src/cli/components/SystemPromptSelector.tsx
Normal file
111
src/cli/components/SystemPromptSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user