Skip to content

Commit 907e41f

Browse files
committed
refactor: Unified HTML rendering with theme-aware RippleUI
Single source of truth for agent response format: - Agents MUST respond with HTML code blocks (```html ... ```) - Removed duplicate ResponseFormatter and HTMLWrapper mechanisms - System prompt now enforces HTML-only responses - Theme-aware styling respects dark/light mode - RippleUI integration prevents theme clashing - OpenCode gets same CLI config support as Claude Code - HTML extracted directly from code blocks - No intermediate conversion steps
1 parent 7626e2f commit 907e41f

3 files changed

Lines changed: 237 additions & 94 deletions

File tree

acp-launcher.js

Lines changed: 17 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,96 +2,40 @@ import { createClient } from 'claude-code-acp';
22
import fs from 'fs';
33
import path from 'path';
44
import os from 'os';
5+
import { default as SYSTEM_PROMPT } from './system-prompt.js';
56

67
/**
78
* Load CLI configuration to ensure identical behavior
9+
* Supports both Claude Code and OpenCode
810
*/
9-
function loadCLIConfig() {
11+
function loadCLIConfig(agentType) {
1012
const configPaths = [
13+
// Claude Code paths
1114
path.join(os.homedir(), '.claude', 'config.json'),
1215
path.join(os.homedir(), '.claude-code', 'config.json'),
13-
path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'claude', 'config.json')
16+
path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'claude', 'config.json'),
17+
// OpenCode paths
18+
path.join(os.homedir(), '.opencode', 'config.json'),
19+
path.join(os.homedir(), '.config', 'opencode', 'config.json'),
20+
path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode', 'config.json')
1421
];
1522

1623
for (const configPath of configPaths) {
1724
try {
1825
if (fs.existsSync(configPath)) {
1926
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
20-
console.log(`[ACP] Loaded CLI config from ${configPath}`);
27+
console.log(`[ACP] Loaded ${agentType} CLI config from ${configPath}`);
2128
return config;
2229
}
2330
} catch (e) {
2431
// Config file doesn't exist or is invalid, continue
2532
}
2633
}
2734

35+
console.log(`[ACP] No ${agentType} config found, using defaults`);
2836
return {};
2937
}
3038

31-
const RIPPLEUI_SYSTEM_PROMPT = `CRITICAL INSTRUCTION: You are responding in a web-based HTML interface. EVERY response must be formatted as beautiful, styled HTML using RippleUI and Tailwind CSS. This is NOT a text-based interface - users see raw HTML rendered in their browser.
32-
33-
YOUR RESPONSE FORMAT MUST BE:
34-
Wrap your ENTIRE response in a single HTML container with these elements:
35-
36-
\`\`\`html
37-
<div class="space-y-4 p-6 max-w-4xl">
38-
<!-- Main content goes here -->
39-
</div>
40-
\`\`\`
41-
42-
STRUCTURE YOUR RESPONSES LIKE THIS:
43-
44-
For questions/answers:
45-
\`\`\`html
46-
<div class="space-y-4 p-6">
47-
<h2 class="text-2xl font-bold text-gray-900">Your Answer</h2>
48-
<div class="card bg-blue-50 border-l-4 border-blue-500 p-4">
49-
<p class="text-gray-700">Your detailed answer here</p>
50-
</div>
51-
</div>
52-
\`\`\`
53-
54-
For code:
55-
\`\`\`html
56-
<div class="space-y-4 p-6">
57-
<h3 class="text-xl font-bold">Code Example</h3>
58-
<pre class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto"><code>// Your code here
59-
function example() { }</code></pre>
60-
</div>
61-
\`\`\`
62-
63-
For lists:
64-
\`\`\`html
65-
<div class="space-y-4 p-6">
66-
<h3 class="text-xl font-bold">Items</h3>
67-
<ul class="list-none space-y-2">
68-
<li class="p-3 bg-gray-100 rounded border-l-4 border-gray-400">• Item one</li>
69-
<li class="p-3 bg-gray-100 rounded border-l-4 border-gray-400">• Item two</li>
70-
</ul>
71-
</div>
72-
\`\`\`
73-
74-
COMPONENT LIBRARY:
75-
- Card: <div class="card bg-white shadow-lg p-6 rounded-lg"><h4 class="font-bold">Title</h4><p>Content</p></div>
76-
- Alert: <div class="alert bg-red-100 border-l-4 border-red-500 p-4"><span class="text-red-800">Warning message</span></div>
77-
- Success: <div class="alert bg-green-100 border-l-4 border-green-500 p-4"><span class="text-green-800">Success</span></div>
78-
- Table: <table class="w-full border-collapse border border-gray-300"><thead class="bg-gray-100"><tr><th class="p-2 text-left">Col</th></tr></thead><tbody><tr><td class="p-2 border border-gray-300">Data</td></tr></tbody></table>
79-
- Badge: <span class="inline-block bg-blue-500 text-white px-3 py-1 rounded-full text-sm">Label</span>
80-
- Code inline: <code class="bg-gray-200 px-2 py-1 rounded text-red-600 font-mono">code</code>
81-
82-
MANDATORY RULES:
83-
✓ EVERY response MUST be wrapped in a div with class "space-y-4 p-6"
84-
✓ Use semantic HTML: <h1>-<h6>, <p>, <ul>, <ol>, <table>, <pre>
85-
✓ Always add Tailwind classes for styling: colors, padding, margins, rounded corners
86-
✓ Code blocks MUST use <pre><code> with language class like \`class="language-javascript"\`
87-
✓ NEVER send plain text without HTML wrapping
88-
✓ NEVER respond outside of HTML container
89-
✓ Use color classes: text-gray-700, bg-blue-50, border-blue-500
90-
✓ Make visual hierarchy clear: use different font sizes, colors, cards
91-
92-
YOU MUST ALWAYS OUTPUT VALID, COMPLETE HTML.
93-
The user's interface shows YOUR HTML directly - make it beautiful, well-organized, and professional.`;
94-
9539
export default class ACPConnection {
9640
constructor() {
9741
this.client = null;
@@ -108,7 +52,7 @@ export default class ACPConnection {
10852
console.log(`[ACP] Connecting to ${agentType}...`);
10953

11054
// Load CLI configuration for identical behavior
111-
const cliConfig = loadCLIConfig();
55+
const cliConfig = loadCLIConfig(agentType);
11256

11357
// Create client with CLI-identical configuration
11458
// Pass through all environment for OAuth and plugin support
@@ -181,14 +125,14 @@ export default class ACPConnection {
181125
}
182126

183127
/**
184-
* Inject skills and system prompt
128+
* Inject unified HTML enforcement system prompt
185129
*/
186130
async injectSkills(additionalContext = '') {
187131
if (!this.client) throw new Error('ACP not connected');
188132

189133
const systemPrompt = additionalContext
190-
? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}`
191-
: RIPPLEUI_SYSTEM_PROMPT;
134+
? `${SYSTEM_PROMPT}\n\n---\n\n${additionalContext}`
135+
: SYSTEM_PROMPT;
192136

193137
return this.client.request('session/skill_inject', {
194138
sessionId: this.sessionId,
@@ -198,14 +142,14 @@ export default class ACPConnection {
198142
}
199143

200144
/**
201-
* Inject system context
145+
* Inject system context with unified HTML enforcement
202146
*/
203147
async injectSystemContext() {
204148
if (!this.client) throw new Error('ACP not connected');
205149

206150
return this.client.request('session/context', {
207151
sessionId: this.sessionId,
208-
context: RIPPLEUI_SYSTEM_PROMPT,
152+
context: SYSTEM_PROMPT,
209153
role: 'system'
210154
});
211155
}

server.js

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import os from 'os';
77
import { execSync } from 'child_process';
88
import { queries } from './database.js';
99
import ACPConnection from './acp-launcher.js';
10-
import { ResponseFormatter } from './response-formatter.js';
11-
import { HTMLWrapper } from './html-wrapper.js';
1210
import { SessionStateStore } from './state-manager.js';
1311
import { StreamHandler } from './stream-handler.js';
1412
import { StateValidator } from './state-validator.js';
@@ -454,27 +452,16 @@ async function processMessage(conversationId, messageId, sessionId, content, age
454452

455453
console.log(`[processMessage] ACP returned: stopReason=${result?.stopReason}, streamUpdates=${streamHandler.getUpdateCount()}`);
456454

457-
// Use full text if available, otherwise use result
458-
let responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
459-
460-
// Only wrap plain text in HTML - don't wrap if already HTML
461-
const isHTML = responseText.trim().startsWith('<');
462-
if (!isHTML) {
463-
responseText = HTMLWrapper.wrapResponse(responseText);
464-
}
465-
466-
// Segment and format
467-
const segments = ResponseFormatter.segmentResponse(responseText);
468-
const metadata = ResponseFormatter.extractMetadata(responseText);
469-
const blocks = streamHandler.getBlocks();
455+
// Agent sends HTML directly - no conversion needed
456+
// Extract HTML code block from response
457+
const responseText = fullText || result?.result || 'No response.';
458+
const htmlMatch = responseText.match(/```html\n([\s\S]*?)\n```/);
459+
const htmlContent = htmlMatch ? htmlMatch[1] : responseText;
470460

471461
const messageContent = {
472-
text: responseText,
473-
blocks: blocks.length > 0 ? blocks : undefined,
474-
segments,
475-
metadata,
476-
streamUpdatesCount: streamHandler.getUpdateCount(),
477-
isHTML: true
462+
text: htmlContent,
463+
html: true,
464+
streamUpdatesCount: streamHandler.getUpdateCount()
478465
};
479466

480467
// Save consolidated response to database

0 commit comments

Comments
 (0)