system prompt
This commit is contained in:
@@ -63,6 +63,10 @@ export const useClaude = (initialSessionId?: string, initialModel?: string | nul
|
||||
if (replaceUrl) window.history.replaceState(null, '', `/chat/${msg.sessionId}`);
|
||||
break;
|
||||
|
||||
case 'system:prompt':
|
||||
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
|
||||
break;
|
||||
|
||||
case 'assistant:partial':
|
||||
streamingRef.current += msg.text;
|
||||
flushStreaming();
|
||||
|
||||
@@ -5,6 +5,26 @@ import { useVisibleOpenCodeModels } from '@/state/useModels';
|
||||
import { useChatSessions } from '@/state/useChatSessions';
|
||||
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 = {
|
||||
replaceUrl?: boolean;
|
||||
taskInfo?: TaskInfo;
|
||||
@@ -68,6 +88,10 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
||||
if (replaceUrl) window.history.replaceState(null, '', `/chat/opencode/${msg.sessionId}`);
|
||||
break;
|
||||
|
||||
case 'system:prompt':
|
||||
setMessages((prev) => [...prev, { role: 'system', text: msg.text }]);
|
||||
break;
|
||||
|
||||
case 'assistant:partial':
|
||||
streamingRef.current += msg.text;
|
||||
flushStreaming();
|
||||
@@ -146,7 +170,7 @@ export const useOpenCode = (initialSessionId?: string, initialModel?: string | n
|
||||
if (!initialSessionId) return;
|
||||
getMessages('opencode', initialSessionId)
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) setMessages(data);
|
||||
if (Array.isArray(data) && data.length > 0) setMessages(splitSystemBlocks(data));
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [initialSessionId]);
|
||||
|
||||
@@ -25,6 +25,7 @@ export type ClientMessage =
|
||||
// Server → Client
|
||||
export type ServerMessage =
|
||||
| { type: 'session:init'; sessionId: string; model: string }
|
||||
| { type: 'system:prompt'; text: string }
|
||||
| { type: 'assistant:text'; text: string }
|
||||
| { type: 'assistant:partial'; text: string }
|
||||
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
||||
|
||||
@@ -135,6 +135,7 @@ async function handleChat({
|
||||
const workingDir = state.cwd ?? homeDir;
|
||||
|
||||
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
|
||||
let promptInput: string | AsyncIterable<SDKUserMessage> = prompt;
|
||||
|
||||
@@ -401,7 +401,8 @@ async function handleChat({ ws, prompt, sessionId, model, attachmentIds, images,
|
||||
}
|
||||
|
||||
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>[] = [];
|
||||
if (images?.length) {
|
||||
|
||||
@@ -11,7 +11,6 @@ type MessageBubbleProps = {
|
||||
};
|
||||
|
||||
export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
||||
const text = formatText(message.text);
|
||||
switch (message.role) {
|
||||
case 'user':
|
||||
return (
|
||||
@@ -20,18 +19,32 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
||||
{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" />
|
||||
))}
|
||||
<div className="whitespace-pre-wrap">{text}</div>
|
||||
<div className="whitespace-pre-wrap">{message.text}</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':
|
||||
if (!text) return null;
|
||||
if (!message.text) return null;
|
||||
return (
|
||||
<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">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||
{text}
|
||||
{message.text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,23 +71,13 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => {
|
||||
return (
|
||||
<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">
|
||||
{text}
|
||||
{message.text}
|
||||
</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 = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export type SessionEntry = {
|
||||
export type ChatMessage =
|
||||
| { role: 'user'; text: string; images?: { filename: string; dataUrl: string }[] }
|
||||
| { role: 'assistant'; text: string }
|
||||
| { role: 'system'; text: string }
|
||||
| {
|
||||
role: 'tool';
|
||||
toolName: string;
|
||||
@@ -29,6 +30,7 @@ export type TaskInfo = {
|
||||
|
||||
export type ServerMessage =
|
||||
| { type: 'session:init'; sessionId: string; model: string }
|
||||
| { type: 'system:prompt'; text: string }
|
||||
| { type: 'assistant:text'; text: string }
|
||||
| { type: 'assistant:partial'; text: string }
|
||||
| { type: 'tool:use'; toolName: string; toolInput: Record<string, unknown>; toolUseId: string }
|
||||
|
||||
Reference in New Issue
Block a user