system prompt

This commit is contained in:
2026-02-17 22:03:02 +00:00
parent 680f623b41
commit cb603e3cf9
7 changed files with 53 additions and 17 deletions
@@ -63,6 +63,10 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`); if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`);
break; break;
case 'system:prompt':
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
break;
case 'assistant:partial': case 'assistant:partial':
streamingRef.current += msg.text; streamingRef.current += msg.text;
flushStreaming(); flushStreaming();
@@ -5,6 +5,26 @@ import { useVisibleOpenCodeModels } from '@/state/useModels';
import { useChatSessions } from '@/state/useChatSessions'; import { useChatSessions } from '@/state/useChatSessions';
import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat'; import type { ChatMessage, ServerMessage, TaskInfo } from 'widgets/Chat';
const SYSTEM_RE = /^<system>([\s\S]*?)<\/system>\s*/;
const splitSystemBlocks = (messages: ChatMessage[]): ChatMessage[] => {
const result: ChatMessage[] = [];
for (const msg of messages) {
if (msg.role !== 'user') {
result.push(msg);
continue;
}
const match = msg.text.match(SYSTEM_RE);
if (!match) {
result.push(msg);
continue;
}
result.push({ role: 'user', text: msg.text.slice(match[0]!.length), images: msg.images });
result.push({ role: 'system', text: match[1]!.trim() });
}
return result;
};
type UseOpenCodeOptions = { type UseOpenCodeOptions = {
replaceUrl?: boolean; replaceUrl?: boolean;
taskInfo?: TaskInfo; taskInfo?: TaskInfo;
@@ -68,6 +88,10 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
if (replaceUrl) window.history.replaceState(null, '', `/chat/opencode/${msg.sessionId}`); if (replaceUrl) window.history.replaceState(null, '', `/chat/opencode/${msg.sessionId}`);
break; break;
case 'system:prompt':
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
break;
case 'assistant:partial': case 'assistant:partial':
streamingRef.current += msg.text; streamingRef.current += msg.text;
flushStreaming(); flushStreaming();
@@ -146,7 +170,7 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
if (!initialSessionId) return; if (!initialSessionId) return;
getMessages('opencode', initialSessionId) getMessages('opencode', initialSessionId)
.then((data) => { .then((data) => {
if (Array.isArray(data) && data.length > 0) setMessages(data); if (Array.isArray(data) && data.length > 0) setMessages(splitSystemBlocks(data));
}) })
.catch(() => {}); .catch(() => {});
}, [initialSessionId]); }, [initialSessionId]);
+1
View File
@@ -25,6 +25,7 @@ export type ClientMessage =
// Server → Client // Server → Client
export type ServerMessage = export type ServerMessage =
| { type: 'session:init'; sessionId: string; model: string } | { type: 'session:init'; sessionId: string; model: string }
| { type: 'system:prompt'; text: string }
| { type: 'assistant:text'; text: string } | { type: 'assistant:text'; text: string }
| { type: 'assistant:partial'; text: string } | { type: 'assistant:partial'; text: string }
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string } | { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
+1
View File
@@ -135,6 +135,7 @@ async function handleChat({
const workingDir = state.cwd ?? homeDir; const workingDir = state.cwd ?? homeDir;
const skillsAppend = await buildSkillsPrompt(ws.data.email); const skillsAppend = await buildSkillsPrompt(ws.data.email);
if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
// Build prompt: use AsyncIterable<SDKUserMessage> with image content blocks when images are present // Build prompt: use AsyncIterable<SDKUserMessage> with image content blocks when images are present
let promptInput: string | AsyncIterable<SDKUserMessage> = prompt; let promptInput: string | AsyncIterable<SDKUserMessage> = prompt;
+2 -1
View File
@@ -401,7 +401,8 @@ async function handleChat({ ws, prompt, sessionId, model, attachmentIds, images,
} }
const skillsAppend = await buildSkillsPrompt(ws.data.email); const skillsAppend = await buildSkillsPrompt(ws.data.email);
const fullPrompt = skillsAppend ? `${skillsAppend}\n\n${prompt}` : prompt; if (skillsAppend) send(ws, { type: 'system:prompt', text: skillsAppend });
const fullPrompt = skillsAppend ? `<system>${skillsAppend}</system>\n\n${prompt}` : prompt;
const parts: Record<string, unknown>[] = []; const parts: Record<string, unknown>[] = [];
if (images?.length) { if (images?.length) {
+18 -15
View File
@@ -11,7 +11,6 @@ type MessageBubbleProps = {
}; };
export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
const text = formatText(message.text);
switch (message.role) { switch (message.role) {
case 'user': case 'user':
return ( return (
@@ -20,18 +19,32 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
{message.images?.map((img, i) => ( {message.images?.map((img, i) => (
<img key={i} src={img.dataUrl} alt={img.filename} className="max-w-full max-h-64 rounded-lg mb-2" /> <img key={i} src={img.dataUrl} alt={img.filename} className="max-w-full max-h-64 rounded-lg mb-2" />
))} ))}
<div className="whitespace-pre-wrap">{text}</div> <div className="whitespace-pre-wrap">{message.text}</div>
</div> </div>
</div> </div>
); );
case 'system':
return (
<div className="flex justify-center">
<details className="max-w-[85%] rounded-2xl bg-duck-dark/5 border border-duck-dark/10 px-4 py-2 text-sm">
<summary className="text-xs text-duck-dark/40 hover:text-duck-dark/60 cursor-pointer select-none">
System prompt
</summary>
<div className="mt-1.5 text-xs text-duck-dark/40 whitespace-pre-wrap border-l-2 border-duck-dark/10 pl-2 max-h-48 overflow-y-auto">
{message.text}
</div>
</details>
</div>
);
case 'assistant': case 'assistant':
if (!text) return null; if (!message.text) return null;
return ( return (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="max-w-[85%] rounded-2xl rounded-tl-sm bg-white/80 border border-duck-dark/10 px-4 py-2.5 text-sm text-duck-dark prose prose-sm max-w-none prose-pre:bg-gray-900 prose-pre:text-green-400 prose-code:text-duck-teal prose-code:before:content-none prose-code:after:content-none"> <div className="max-w-[85%] rounded-2xl rounded-tl-sm bg-white/80 border border-duck-dark/10 px-4 py-2.5 text-sm text-duck-dark prose prose-sm max-w-none prose-pre:bg-gray-900 prose-pre:text-green-400 prose-code:text-duck-teal prose-code:before:content-none prose-code:after:content-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}> <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{text} {message.text}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</div> </div>
@@ -58,23 +71,13 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
return ( return (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="max-w-[80%] rounded-2xl bg-red-50 border border-red-200 px-4 py-2.5 text-sm text-red-700"> <div className="max-w-[80%] rounded-2xl bg-red-50 border border-red-200 px-4 py-2.5 text-sm text-red-700">
{text} {message.text}
</div> </div>
</div> </div>
); );
} }
}; };
function formatText(value: unknown): string {
if (typeof value === 'string') return value;
if (value == null) return '';
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
type StreamingBubbleProps = { type StreamingBubbleProps = {
text: string; text: string;
}; };
+2
View File
@@ -9,6 +9,7 @@ export type SessionEntry = {
export type ChatMessage = export type ChatMessage =
| { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] } | { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] }
| { role: 'assistant'; text: string } | { role: 'assistant'; text: string }
| { role: 'system'; text: string }
| { | {
role: 'tool'; role: 'tool';
toolName: string; toolName: string;
@@ -29,6 +30,7 @@ export type TaskInfo = {
export type ServerMessage = export type ServerMessage =
| { type: 'session:init'; sessionId: string; model: string } | { type: 'session:init'; sessionId: string; model: string }
| { type: 'system:prompt'; text: string }
| { type: 'assistant:text'; text: string } | { type: 'assistant:text'; text: string }
| { type: 'assistant:partial'; text: string } | { type: 'assistant:partial'; text: string }
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string } | { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }