From d563e564ad5be010de4514c0dedcb223b1740f33 Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Thu, 29 Jan 2026 21:31:32 +0000 Subject: [PATCH] shelley/ui: Add collapsible system prompt viewer at top of conversation Prompt: (In a new worktree) At the very top of the conversation/timeline view, add a way to "show" the system prompt, which I think the UI has. Let it be expanded to see it. Add a SystemPromptView component that: - Shows at the very top of the conversation/timeline view - Displays system prompt info (line count, size in KB) - Is collapsed by default - Expands to show full system prompt text when clicked The viewer uses the same styling pattern as other expandable tool components. Co-authored-by: Shelley --- ui/src/components/ChatInterface.tsx | 10 +++- ui/src/components/SystemPromptView.tsx | 83 ++++++++++++++++++++++++++ ui/src/styles.css | 81 +++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/SystemPromptView.tsx diff --git a/ui/src/components/ChatInterface.tsx b/ui/src/components/ChatInterface.tsx index 9307f614b54dc38bb088b6e024fa6abed29a1788..6ee249b7532befb764d1fd875128d4c8a8cf5c28 100644 --- a/ui/src/components/ChatInterface.tsx +++ b/ui/src/components/ChatInterface.tsx @@ -29,6 +29,7 @@ import DirectoryPickerModal from "./DirectoryPickerModal"; import { useVersionChecker } from "./VersionChecker"; import TerminalWidget from "./TerminalWidget"; import ModelPicker from "./ModelPicker"; +import SystemPromptView from "./SystemPromptView"; // Ephemeral terminal instance (not persisted to database) interface EphemeralTerminal { @@ -1251,8 +1252,15 @@ function ChatInterface({ return null; }); + // Find system message to render at the top + const systemMessage = messages.find((m) => m.type === "system"); + // Append ephemeral terminals at the end - return [...rendered, ...terminalElements]; + return [ + systemMessage && , + ...rendered, + ...terminalElements, + ]; }; return ( diff --git a/ui/src/components/SystemPromptView.tsx b/ui/src/components/SystemPromptView.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7a231d4c5c8ef28c5c1ca3755d642f6b8b232d3b --- /dev/null +++ b/ui/src/components/SystemPromptView.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import { Message, LLMContent } from "../types"; + +interface SystemPromptViewProps { + message: Message; +} + +function SystemPromptView({ message }: SystemPromptViewProps) { + const [isExpanded, setIsExpanded] = useState(false); + + // Extract system prompt text from llm_data + let systemPromptText = ""; + if (message.llm_data) { + try { + const llmData = + typeof message.llm_data === "string" ? JSON.parse(message.llm_data) : message.llm_data; + if (llmData && llmData.Content && Array.isArray(llmData.Content)) { + const textContent = llmData.Content.find((c: LLMContent) => c.Type === 2 && c.Text); + if (textContent) { + systemPromptText = textContent.Text; + } + } + } catch (err) { + console.error("Failed to parse system prompt:", err); + } + } + + if (!systemPromptText) { + return null; + } + + // Count lines and approximate size + const lineCount = systemPromptText.split("\n").length; + const charCount = systemPromptText.length; + const sizeKb = (charCount / 1024).toFixed(1); + + return ( +
+
setIsExpanded(!isExpanded)}> +
+ 📋 + System Prompt + + {lineCount} lines, {sizeKb} KB + +
+ +
+ + {isExpanded && ( +
+
{systemPromptText}
+
+ )} +
+ ); +} + +export default SystemPromptView; diff --git a/ui/src/styles.css b/ui/src/styles.css index fbaba0c0c3f618806242685cb3c8775de84bd16c..9a2c2dd0fe1988f37f9df2778e92e5a589ab26a2 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -4801,3 +4801,84 @@ svg { margin-top: 1rem; padding-bottom: 0.5rem; } + +/* System Prompt View */ +.system-prompt-view { + background: var(--gray-100); + border-radius: 0.5rem; + margin: 0.5rem 0; + width: 100%; + border: 1px dashed var(--border); +} + +.dark .system-prompt-view { + background: var(--gray-800); +} + +.system-prompt-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + cursor: pointer; + user-select: none; +} + +.system-prompt-header:hover { + background: rgba(0, 0, 0, 0.02); + border-radius: 0.5rem; +} + +.dark .system-prompt-header:hover { + background: rgba(255, 255, 255, 0.02); +} + +.system-prompt-summary { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; + min-width: 0; +} + +.system-prompt-icon { + font-size: 1rem; + flex-shrink: 0; +} + +.system-prompt-label { + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-primary); + font-weight: 500; +} + +.system-prompt-meta { + font-size: 0.75rem; + color: var(--text-secondary); + flex-shrink: 0; +} + +.system-prompt-content { + padding: 0 1rem 1rem 1rem; +} + +.system-prompt-text { + font-family: var(--font-mono); + font-size: 0.75rem; + line-height: 1.5; + color: var(--text-secondary); + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: 0.375rem; + padding: 1rem; + margin: 0; + max-height: 400px; + overflow: auto; + white-space: pre-wrap; + word-wrap: break-word; +} + +.dark .system-prompt-text { + background: var(--gray-900); +}