diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts b/src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts index 1744d671..cdcc234e 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts +++ b/src/apps/officer-web/Screens/Dashboard/Chat/useClaude.ts @@ -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(); diff --git a/src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts b/src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts index 8b398912..acb773bc 100644 --- a/src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts +++ b/src/apps/officer-web/Screens/Dashboard/Chat/useOpenCode.ts @@ -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 = /^([\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]); diff --git a/src/servers/api/chat-types.ts b/src/servers/api/chat-types.ts index 63c3bda6..e23ca420 100644 --- a/src/servers/api/chat-types.ts +++ b/src/servers/api/chat-types.ts @@ -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; toolUseId: string } diff --git a/src/servers/api/claude/websocket.ts b/src/servers/api/claude/websocket.ts index e7655746..1693d300 100644 --- a/src/servers/api/claude/websocket.ts +++ b/src/servers/api/claude/websocket.ts @@ -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 with image content blocks when images are present let promptInput: string | AsyncIterable = prompt; diff --git a/src/servers/api/opencode/websocket.ts b/src/servers/api/opencode/websocket.ts index 48db2175..b5b456c4 100644 --- a/src/servers/api/opencode/websocket.ts +++ b/src/servers/api/opencode/websocket.ts @@ -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 ? `${skillsAppend}\n\n${prompt}` : prompt; const parts: Record[] = []; if (images?.length) { diff --git a/src/workspaces/widgets/Chat/MessageBubble.tsx b/src/workspaces/widgets/Chat/MessageBubble.tsx index 3bb598bf..6304da96 100644 --- a/src/workspaces/widgets/Chat/MessageBubble.tsx +++ b/src/workspaces/widgets/Chat/MessageBubble.tsx @@ -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.filename} ))} -
{text}
+
{message.text}
); + case 'system': + return ( +
+
+ + System prompt + +
+ {message.text} +
+
+
+ ); + case 'assistant': - if (!text) return null; + if (!message.text) return null; return (
- {text} + {message.text}
@@ -58,23 +71,13 @@ export const MessageBubble = ({ message, onAnswer }: MessageBubbleProps) => { return (
- {text} + {message.text}
); } }; -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; }; diff --git a/src/workspaces/widgets/Chat/types.ts b/src/workspaces/widgets/Chat/types.ts index ecb85fae..e469bbd2 100644 --- a/src/workspaces/widgets/Chat/types.ts +++ b/src/workspaces/widgets/Chat/types.ts @@ -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; toolUseId: string }