shelley/ui: Add collapsible system prompt viewer at top of conversation
Philip Zeyliger
and
Shelley
created 3 months ago
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 <shelley@exe.dev>
Change summary
ui/src/components/ChatInterface.tsx | 10 +++
ui/src/components/SystemPromptView.tsx | 83 ++++++++++++++++++++++++++++
ui/src/styles.css | 81 +++++++++++++++++++++++++++
3 files changed, 173 insertions(+), 1 deletion(-)
Detailed changes
@@ -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 && <SystemPromptView key="system-prompt" message={systemMessage} />,
+ ...rendered,
+ ...terminalElements,
+ ];
};
return (
@@ -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 (
+ <div className="system-prompt-view">
+ <div className="system-prompt-header" onClick={() => setIsExpanded(!isExpanded)}>
+ <div className="system-prompt-summary">
+ <span className="system-prompt-icon">📋</span>
+ <span className="system-prompt-label">System Prompt</span>
+ <span className="system-prompt-meta">
+ {lineCount} lines, {sizeKb} KB
+ </span>
+ </div>
+ <button
+ className="tool-toggle"
+ aria-label={isExpanded ? "Collapse" : "Expand"}
+ aria-expanded={isExpanded}
+ >
+ <svg
+ width="12"
+ height="12"
+ viewBox="0 0 12 12"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ style={{
+ transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
+ transition: "transform 0.2s",
+ }}
+ >
+ <path
+ d="M4.5 3L7.5 6L4.5 9"
+ stroke="currentColor"
+ strokeWidth="1.5"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ />
+ </svg>
+ </button>
+ </div>
+
+ {isExpanded && (
+ <div className="system-prompt-content">
+ <pre className="system-prompt-text">{systemPromptText}</pre>
+ </div>
+ )}
+ </div>
+ );
+}
+
+export default SystemPromptView;
@@ -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);
+}