agentgui 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/acp-launcher.js CHANGED
@@ -33,19 +33,62 @@ function findBinary(paths) {
33
33
  return null;
34
34
  }
35
35
 
36
- const RIPPLEUI_SYSTEM_PROMPT = `ALWAYS respond with HTML using RippleUI components. The chat renders HTML. Use: cards (class='card'), alerts (class='alert alert-info'), tables (class='table table-zebra'), badges (class='badge badge-primary'), buttons (class='btn btn-primary'). Wrap all responses in styled HTML with Tailwind CSS utility classes for layout.
37
-
38
- RIPPLEUI COMPONENTS:
39
- Cards: <div class="card bg-base-100 shadow-lg p-6"><h2 class="text-xl font-bold mb-2">Title</h2><p>Content</p></div>
40
- Alerts: <div class="alert alert-info"><span>Message</span></div>
41
- Tables: <div class="overflow-x-auto"><table class="table table-zebra"><thead><tr><th>Col</th></tr></thead><tbody><tr><td>Val</td></tr></tbody></table></div>
42
- Badges: <span class="badge badge-primary">Tag</span>
43
- Buttons: <button class="btn btn-primary">Action</button>
44
- Code: <pre class="bg-base-200 p-4 rounded-lg overflow-x-auto"><code>code here</code></pre>
45
- Lists: <ul class="list-none space-y-2"><li class="p-3 bg-base-200 rounded-lg">Item</li></ul>
46
-
47
- Use Tailwind CSS utility classes for layout (flex, grid, gap-4, p-4, rounded, shadow).
48
- ALWAYS wrap responses in styled HTML. Never send plain unstyled text.`;
36
+ const RIPPLEUI_SYSTEM_PROMPT = `CRITICAL: ALWAYS respond with styled HTML using RippleUI components. This is a web-based chat interface that ONLY renders HTML beautifully. NEVER respond with plain text.
37
+
38
+ FOR EVERY RESPONSE:
39
+ 1. Wrap your ENTIRE response in a container div with proper styling
40
+ 2. Use semantic HTML (h1-h6 for headings, p for paragraphs, ul/ol for lists)
41
+ 3. Format code in <pre><code> blocks with language class
42
+ 4. Use cards for organized information blocks
43
+ 5. Use alerts for important information
44
+ 6. Use tables for tabular data
45
+ 7. Style everything with Tailwind CSS classes
46
+
47
+ RESPONSE STRUCTURE TEMPLATE:
48
+ \`\`\`html
49
+ <div class="space-y-4 p-4">
50
+ <h2 class="text-2xl font-bold">Main Heading</h2>
51
+ <p class="text-gray-700">Introductory text here.</p>
52
+
53
+ <div class="card bg-base-100 shadow-lg p-4">
54
+ <h3 class="text-lg font-semibold mb-2">Section Title</h3>
55
+ <p>Content goes here.</p>
56
+ </div>
57
+
58
+ <pre class="bg-base-200 p-4 rounded-lg overflow-x-auto"><code class="language-javascript">// Code example
59
+ const example = () => { };</code></pre>
60
+
61
+ <ul class="list-none space-y-2 ml-4">
62
+ <li class="p-2 bg-base-200 rounded">• Item one</li>
63
+ <li class="p-2 bg-base-200 rounded">• Item two</li>
64
+ </ul>
65
+ </div>
66
+ \`\`\`
67
+
68
+ COMPONENT REFERENCE:
69
+ - Cards: <div class="card bg-base-100 shadow-lg p-6"><h3 class="font-bold">Title</h3><p>Content</p></div>
70
+ - Alerts: <div class="alert alert-info"><span>Message</span></div>
71
+ - Tables: <table class="table table-zebra"><thead><tr><th>Header</th></tr></thead><tbody><tr><td>Data</td></tr></tbody></table>
72
+ - Badges: <span class="badge badge-primary">Label</span>
73
+ - Code: <pre class="bg-base-200 p-4 rounded-lg overflow-x-auto"><code class="language-{lang}">{code}</code></pre>
74
+ - Lists: <ul class="list-none space-y-2"><li class="p-2 bg-base-200 rounded">Item</li></ul>
75
+
76
+ REQUIRED FOR ALL RESPONSES:
77
+ ✓ Wrap response in HTML (usually a div with space-y-4 for vertical spacing)
78
+ ✓ Use Tailwind classes: text-xl, font-bold, p-4, mb-3, rounded-lg, shadow, etc.
79
+ ✓ Always use proper semantic HTML tags
80
+ ✓ Format code blocks with language class for syntax highlighting
81
+ ✓ Use colors: bg-blue-50, text-gray-700, border-blue-200 for visual hierarchy
82
+ ✓ NEVER respond with raw text - ALWAYS wrap in HTML container
83
+
84
+ EXAMPLES OF GOOD RESPONSES:
85
+ - Questions: Wrap answer in a card with bold question and styled answer
86
+ - Code: Use <pre><code> with language class
87
+ - Lists: Use <ul> or <ol> with proper styling
88
+ - Instructions: Use numbered or bulleted lists in cards
89
+ - Explanations: Use cards for each concept, bold headings, clear spacing
90
+
91
+ Remember: The user sees raw HTML output. Make it beautiful and readable.`;
49
92
 
50
93
  export default class ACPConnection {
51
94
  constructor() {
@@ -255,13 +298,44 @@ export default class ACPConnection {
255
298
  return this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId });
256
299
  }
257
300
 
258
- async injectSkills() {
301
+ async injectSkills(additionalContext = '') {
259
302
  if (this.printMode) return {};
260
- return this.sendRequest('session/skill_inject', {
261
- sessionId: this.sessionId,
262
- skills: [],
263
- notification: [{ type: 'text', text: RIPPLEUI_SYSTEM_PROMPT }]
264
- }).catch(() => null);
303
+
304
+ // Combine the system prompt with any additional context
305
+ const systemPrompt = additionalContext ? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}` : RIPPLEUI_SYSTEM_PROMPT;
306
+
307
+ try {
308
+ const result = await this.sendRequest('session/skill_inject', {
309
+ sessionId: this.sessionId,
310
+ skills: [],
311
+ notification: [{ type: 'text', text: systemPrompt }]
312
+ });
313
+ return result;
314
+ } catch (e) {
315
+ // skill_inject may not be supported, try alternative methods
316
+ console.log(`[ACP] skill_inject failed (may not be supported): ${e.message}`);
317
+ return null;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Inject system prompt as initial context
323
+ */
324
+ async injectSystemContext() {
325
+ if (this.printMode) return {};
326
+
327
+ // Some versions may need the system prompt sent as the first message context
328
+ try {
329
+ await this.sendRequest('session/context', {
330
+ sessionId: this.sessionId,
331
+ context: RIPPLEUI_SYSTEM_PROMPT,
332
+ role: 'system'
333
+ });
334
+ return { success: true };
335
+ } catch (e) {
336
+ // This method may not be supported, silently fail
337
+ return null;
338
+ }
265
339
  }
266
340
 
267
341
  async sendPrompt(prompt) {
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Hot Reload Manager
3
+ * Enables live reloading of client code and graceful server restarts without losing connections
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ export class HotReloadManager {
13
+ constructor(staticDir, options = {}) {
14
+ this.staticDir = staticDir;
15
+ this.watchedFiles = new Map();
16
+ this.hotReloadClients = [];
17
+ this.debounceTimers = new Map();
18
+ this.debounceDelay = options.debounceDelay || 300;
19
+ this.enabled = options.enabled !== false;
20
+ }
21
+
22
+ /**
23
+ * Start watching files for changes
24
+ */
25
+ start() {
26
+ if (!this.enabled) return;
27
+
28
+ try {
29
+ this.watchDirectory(this.staticDir);
30
+ console.log('[HotReload] Watching for changes:', this.staticDir);
31
+ } catch (e) {
32
+ console.error('[HotReload] Failed to start:', e.message);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Watch directory recursively
38
+ */
39
+ watchDirectory(dir) {
40
+ try {
41
+ const files = fs.readdirSync(dir, { withFileTypes: true });
42
+
43
+ for (const file of files) {
44
+ const fullPath = path.join(dir, file.name);
45
+
46
+ if (file.isDirectory()) {
47
+ this.watchDirectory(fullPath);
48
+ } else {
49
+ this.watchFile(fullPath);
50
+ }
51
+ }
52
+ } catch (e) {
53
+ console.error('[HotReload] Error watching directory:', e.message);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Watch individual file for changes
59
+ */
60
+ watchFile(filePath) {
61
+ if (this.watchedFiles.has(filePath)) return;
62
+
63
+ try {
64
+ fs.watchFile(filePath, { interval: 100 }, (curr, prev) => {
65
+ if (curr.mtime > prev.mtime) {
66
+ this.onFileChanged(filePath);
67
+ }
68
+ });
69
+
70
+ this.watchedFiles.set(filePath, true);
71
+ } catch (e) {
72
+ console.error('[HotReload] Error watching file:', e.message);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Handle file change with debouncing
78
+ */
79
+ onFileChanged(filePath) {
80
+ // Clear existing timer for this file
81
+ if (this.debounceTimers.has(filePath)) {
82
+ clearTimeout(this.debounceTimers.get(filePath));
83
+ }
84
+
85
+ // Set new debounced timer
86
+ const timer = setTimeout(() => {
87
+ this.debounceTimers.delete(filePath);
88
+ const relPath = path.relative(this.staticDir, filePath);
89
+
90
+ console.log(`[HotReload] File changed: ${relPath}`);
91
+ this.broadcastReload();
92
+ }, this.debounceDelay);
93
+
94
+ this.debounceTimers.set(filePath, timer);
95
+ }
96
+
97
+ /**
98
+ * Register a WebSocket client for hot reload
99
+ */
100
+ registerClient(ws) {
101
+ if (!this.enabled) return;
102
+ this.hotReloadClients.push(ws);
103
+
104
+ ws.on('close', () => {
105
+ const idx = this.hotReloadClients.indexOf(ws);
106
+ if (idx > -1) this.hotReloadClients.splice(idx, 1);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Broadcast reload signal to all connected clients
112
+ */
113
+ broadcastReload() {
114
+ const message = JSON.stringify({ type: 'reload', timestamp: Date.now() });
115
+
116
+ for (const ws of this.hotReloadClients) {
117
+ if (ws.readyState === 1) { // WebSocket.OPEN
118
+ try {
119
+ ws.send(message);
120
+ } catch (e) {
121
+ // Client may have disconnected
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Cleanup watchers
129
+ */
130
+ stop() {
131
+ for (const filePath of this.watchedFiles.keys()) {
132
+ try {
133
+ fs.unwatchFile(filePath);
134
+ } catch (e) {}
135
+ }
136
+
137
+ for (const timer of this.debounceTimers.values()) {
138
+ clearTimeout(timer);
139
+ }
140
+
141
+ this.watchedFiles.clear();
142
+ this.debounceTimers.clear();
143
+ this.hotReloadClients = [];
144
+ }
145
+
146
+ /**
147
+ * Get HTML snippet to inject for hot reload
148
+ */
149
+ getClientScript(baseUrl = '') {
150
+ if (!this.enabled) return '';
151
+
152
+ return `
153
+ <script>
154
+ (function() {
155
+ const baseUrl = '${baseUrl}';
156
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + baseUrl + '/hot-reload');
157
+
158
+ ws.onmessage = function(event) {
159
+ try {
160
+ const data = JSON.parse(event.data);
161
+ if (data.type === 'reload') {
162
+ console.log('[HotReload] Reloading page...');
163
+ location.reload();
164
+ }
165
+ } catch (e) {
166
+ console.error('[HotReload] Error parsing message:', e);
167
+ }
168
+ };
169
+
170
+ ws.onerror = function(e) {
171
+ console.log('[HotReload] WebSocket error:', e);
172
+ };
173
+
174
+ ws.onclose = function() {
175
+ console.log('[HotReload] Connection closed, will attempt to reconnect...');
176
+ setTimeout(function() {
177
+ location.reload();
178
+ }, 2000);
179
+ };
180
+ })();
181
+ </script>
182
+ `;
183
+ }
184
+ }
185
+
186
+ export default HotReloadManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Response formatter for Claude Code outputs
3
+ * Handles segmentation of text, code blocks, tool calls, thinking blocks, etc.
4
+ */
5
+
6
+ export class ResponseFormatter {
7
+ /**
8
+ * Parse Claude Code response into structured segments
9
+ */
10
+ static parseResponse(text) {
11
+ if (!text || typeof text !== 'string') return [];
12
+
13
+ const segments = [];
14
+ const lines = text.split('\n');
15
+ let current = null;
16
+ let codeBlockLang = null;
17
+ let inCodeBlock = false;
18
+
19
+ for (let i = 0; i < lines.length; i++) {
20
+ const line = lines[i];
21
+
22
+ // Check for code block markers
23
+ const codeBlockMatch = line.match(/^```(\w+)?$/);
24
+ if (codeBlockMatch) {
25
+ if (!inCodeBlock) {
26
+ // Starting a code block
27
+ if (current && current.type === 'text' && current.content.trim()) {
28
+ segments.push(current);
29
+ current = null;
30
+ }
31
+ inCodeBlock = true;
32
+ codeBlockLang = codeBlockMatch[1] || 'text';
33
+ current = { type: 'code', language: codeBlockLang, content: '' };
34
+ } else {
35
+ // Ending a code block
36
+ if (current) {
37
+ segments.push(current);
38
+ current = null;
39
+ }
40
+ inCodeBlock = false;
41
+ codeBlockLang = null;
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if (inCodeBlock) {
47
+ current.content += (current.content ? '\n' : '') + line;
48
+ } else {
49
+ // Check for markdown formatting
50
+ if (line.match(/^#+\s/)) {
51
+ // Heading
52
+ if (current && current.type === 'text' && current.content.trim()) {
53
+ segments.push(current);
54
+ }
55
+ segments.push({ type: 'heading', level: line.match(/^#+/)[0].length, content: line.replace(/^#+\s/, '') });
56
+ current = null;
57
+ } else if (line.match(/^>\s/) || line.match(/^-\s/) || line.match(/^\d+\.\s/)) {
58
+ // Quote, bullet, or numbered list
59
+ if (current && current.type === 'text') {
60
+ segments.push(current);
61
+ }
62
+ if (line.match(/^>\s/)) {
63
+ segments.push({ type: 'blockquote', content: line.replace(/^>\s/, '') });
64
+ } else {
65
+ segments.push({ type: 'list_item', content: line.replace(/^[-\d+.]\s+/, '') });
66
+ }
67
+ current = null;
68
+ } else if (line.trim()) {
69
+ // Regular text
70
+ if (!current || current.type !== 'text') {
71
+ if (current) segments.push(current);
72
+ current = { type: 'text', content: line };
73
+ } else {
74
+ current.content += '\n' + line;
75
+ }
76
+ } else if (current && current.type === 'text' && current.content.trim()) {
77
+ // Empty line - could indicate paragraph break
78
+ current.content += '\n\n';
79
+ }
80
+ }
81
+ }
82
+
83
+ if (current) {
84
+ segments.push(current);
85
+ }
86
+
87
+ return segments;
88
+ }
89
+
90
+ /**
91
+ * Extract tool calls, thinking blocks, and task information
92
+ */
93
+ static extractMetadata(text) {
94
+ if (!text || typeof text !== 'string') return { tools: [], thinking: [], tasks: [] };
95
+
96
+ const metadata = {
97
+ tools: [],
98
+ thinking: [],
99
+ tasks: [],
100
+ subagents: []
101
+ };
102
+
103
+ // Find tool call patterns
104
+ const toolPattern = /(?:^|\n)\s*(?:Using|Calling|Invoking|Running)\s+(?:the\s+)?(\w+(?:\s+\w+)*?)(?:\s+(?:tool|command|function))?\s*(?:with|to)?\s*(.+?)(?:\n|$)/gi;
105
+ let match;
106
+ while ((match = toolPattern.exec(text)) !== null) {
107
+ metadata.tools.push({
108
+ name: match[1].trim(),
109
+ description: match[2]?.trim() || ''
110
+ });
111
+ }
112
+
113
+ // Find thinking/reasoning blocks
114
+ const thinkingPattern = /(?:thinking|reasoning|analyzing|considering)[\s:]+(.+?)(?:\n\n|$)/gi;
115
+ while ((match = thinkingPattern.exec(text)) !== null) {
116
+ metadata.thinking.push(match[1].trim());
117
+ }
118
+
119
+ // Find task references
120
+ const taskPattern = /(?:task|step|doing)[\s:]+(.+?)(?:\n|$)/gi;
121
+ while ((match = taskPattern.exec(text)) !== null) {
122
+ metadata.tasks.push(match[1].trim());
123
+ }
124
+
125
+ // Find subagent references
126
+ const subagentPattern = /(?:using|with|via)\s+(?:the\s+)?(\w+)\s+(?:subagent|agent)/gi;
127
+ while ((match = subagentPattern.exec(text)) !== null) {
128
+ metadata.subagents.push(match[1].trim());
129
+ }
130
+
131
+ return metadata;
132
+ }
133
+
134
+ /**
135
+ * Segment a response into logical parts
136
+ * Splits on natural boundaries like tool calls, thinking, etc.
137
+ */
138
+ static segmentResponse(text) {
139
+ if (!text || typeof text !== 'string') return [];
140
+
141
+ const segments = [];
142
+ const parts = [];
143
+
144
+ // Split by major transitions
145
+ const transitions = [
146
+ { pattern: /I'll\s+use\s+the\s+\w+\s+(?:subagent|tool|command)/i, type: 'tool_transition' },
147
+ { pattern: /Let\s+me\s+(?:use|run|execute|call)\s+/i, type: 'action_start' },
148
+ { pattern: /Here['s]*\s+(?:what|the|a)\s+(?:happened|result|output)/i, type: 'result' },
149
+ { pattern: /^(✓|✅|✗|❌|-|•)\s+/m, type: 'bullet' }
150
+ ];
151
+
152
+ let currentPart = '';
153
+ const lines = text.split('\n');
154
+
155
+ for (const line of lines) {
156
+ let isTransition = false;
157
+
158
+ for (const { pattern, type } of transitions) {
159
+ if (pattern.test(line)) {
160
+ if (currentPart.trim()) {
161
+ parts.push({ text: currentPart.trim(), type: 'text' });
162
+ currentPart = '';
163
+ }
164
+ isTransition = true;
165
+ break;
166
+ }
167
+ }
168
+
169
+ if (isTransition || line.match(/^#+\s/) || line.match(/^```/)) {
170
+ if (currentPart.trim()) {
171
+ parts.push({ text: currentPart.trim(), type: 'text' });
172
+ currentPart = '';
173
+ }
174
+ }
175
+
176
+ currentPart += (currentPart ? '\n' : '') + line;
177
+ }
178
+
179
+ if (currentPart.trim()) {
180
+ parts.push({ text: currentPart.trim(), type: 'text' });
181
+ }
182
+
183
+ // Further parse each part
184
+ for (const part of parts) {
185
+ segments.push({
186
+ ...part,
187
+ parsed: this.parseResponse(part.text),
188
+ metadata: this.extractMetadata(part.text)
189
+ });
190
+ }
191
+
192
+ return segments;
193
+ }
194
+
195
+ /**
196
+ * Format segments for display with proper HTML
197
+ */
198
+ static formatForDisplay(segments) {
199
+ if (!Array.isArray(segments)) return '';
200
+
201
+ const html = [];
202
+
203
+ for (const segment of segments) {
204
+ if (segment.type === 'code') {
205
+ html.push(`<pre class="code-block language-${segment.language}"><code>${this.escapeHtml(segment.content)}</code></pre>`);
206
+ } else if (segment.type === 'heading') {
207
+ const tag = `h${Math.min(segment.level, 6)}`;
208
+ html.push(`<${tag} class="response-heading">${this.escapeHtml(segment.content)}</${tag}>`);
209
+ } else if (segment.type === 'blockquote') {
210
+ html.push(`<blockquote class="response-quote">${this.escapeHtml(segment.content)}</blockquote>`);
211
+ } else if (segment.type === 'list_item') {
212
+ html.push(`<li class="response-list-item">${this.escapeHtml(segment.content)}</li>`);
213
+ } else if (segment.type === 'text') {
214
+ html.push(`<p class="response-text">${this.escapeHtml(segment.content).replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\*(.*?)\*/g, '<em>$1</em>')}</p>`);
215
+ }
216
+ }
217
+
218
+ return html.join('\n');
219
+ }
220
+
221
+ static escapeHtml(text) {
222
+ if (typeof text !== 'string') return '';
223
+ return text
224
+ .replace(/&/g, '&amp;')
225
+ .replace(/</g, '&lt;')
226
+ .replace(/>/g, '&gt;')
227
+ .replace(/"/g, '&quot;')
228
+ .replace(/'/g, '&#039;');
229
+ }
230
+
231
+ /**
232
+ * Create rich metadata display
233
+ */
234
+ static createMetadataDisplay(metadata) {
235
+ if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
236
+ return null;
237
+ }
238
+
239
+ const html = ['<div class="response-metadata">'];
240
+
241
+ if (metadata.tools?.length) {
242
+ html.push('<div class="metadata-section tools">');
243
+ html.push('<strong>Tools Used:</strong>');
244
+ html.push('<ul>');
245
+ for (const tool of metadata.tools) {
246
+ html.push(`<li><code>${this.escapeHtml(tool.name)}</code>${tool.description ? ': ' + this.escapeHtml(tool.description) : ''}</li>`);
247
+ }
248
+ html.push('</ul></div>');
249
+ }
250
+
251
+ if (metadata.thinking?.length) {
252
+ html.push('<details class="metadata-section thinking">');
253
+ html.push('<summary>Reasoning</summary>');
254
+ for (const thought of metadata.thinking) {
255
+ html.push(`<p>${this.escapeHtml(thought)}</p>`);
256
+ }
257
+ html.push('</details>');
258
+ }
259
+
260
+ if (metadata.subagents?.length) {
261
+ html.push('<div class="metadata-section subagents">');
262
+ html.push('<strong>Subagents:</strong>');
263
+ html.push('<ul>');
264
+ for (const agent of metadata.subagents) {
265
+ html.push(`<li>${this.escapeHtml(agent)}</li>`);
266
+ }
267
+ html.push('</ul></div>');
268
+ }
269
+
270
+ if (metadata.tasks?.length) {
271
+ html.push('<div class="metadata-section tasks">');
272
+ html.push('<strong>Tasks:</strong>');
273
+ html.push('<ul>');
274
+ for (const task of metadata.tasks) {
275
+ html.push(`<li>${this.escapeHtml(task)}</li>`);
276
+ }
277
+ html.push('</ul></div>');
278
+ }
279
+
280
+ html.push('</div>');
281
+ return html.join('\n');
282
+ }
283
+ }
284
+
285
+ export default ResponseFormatter;
package/server.js CHANGED
@@ -7,6 +7,7 @@ import os from 'os';
7
7
  import { execSync } from 'child_process';
8
8
  import { queries } from './database.js';
9
9
  import ACPConnection from './acp-launcher.js';
10
+ import { ResponseFormatter } from './response-formatter.js';
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
  const PORT = process.env.PORT || 3000;
@@ -31,7 +32,9 @@ async function getACP(agentId, cwd) {
31
32
  await conn.initialize();
32
33
  await conn.newSession(cwd);
33
34
  await conn.setSessionMode('bypassPermissions');
34
- await conn.injectSkills(['html_rendering', 'image_display', 'scrot', 'fs_access']);
35
+ // Inject system prompt to ensure HTML/RippleUI formatting
36
+ await conn.injectSkills();
37
+ await conn.injectSystemContext();
35
38
  acpPool.set(agentId, conn);
36
39
  console.log(`ACP connection ready for ${agentId} in ${cwd}`);
37
40
  return conn;
@@ -313,17 +316,21 @@ async function processMessage(conversationId, messageId, sessionId, content, age
313
316
 
314
317
  let fullText = '';
315
318
  const blocks = [];
319
+ const updateChunks = []; // Track all message chunks in order
316
320
  conn.onUpdate = (params) => {
317
321
  const u = params.update;
318
322
  if (!u) return;
319
323
  const kind = u.sessionUpdate;
320
324
  if (kind === 'agent_message_chunk' && u.content?.text) {
321
325
  fullText += u.content.text;
326
+ updateChunks.push({ type: 'text', content: u.content.text, timestamp: Date.now() });
322
327
  } else if (kind === 'html_content' && u.content?.html) {
323
328
  blocks.push({ type: 'html', html: u.content.html, title: u.content.title, id: u.content.id });
329
+ updateChunks.push({ type: 'html', content: u.content.html, title: u.content.title, timestamp: Date.now() });
324
330
  } else if (kind === 'image_content' && u.content?.path) {
325
331
  const imageUrl = BASE_URL + '/api/image/' + encodeURIComponent(u.content.path);
326
332
  blocks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, alt: u.content.alt });
333
+ updateChunks.push({ type: 'image', path: u.content.path, url: imageUrl, title: u.content.title, timestamp: Date.now() });
327
334
  }
328
335
  };
329
336
 
@@ -331,7 +338,23 @@ async function processMessage(conversationId, messageId, sessionId, content, age
331
338
  conn.onUpdate = null;
332
339
 
333
340
  const responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
334
- const messageContent = blocks.length > 0 ? { text: responseText, blocks } : responseText;
341
+
342
+ // Segment and format the response for better display
343
+ const segments = ResponseFormatter.segmentResponse(responseText);
344
+ const metadata = ResponseFormatter.extractMetadata(responseText);
345
+
346
+ const messageContent = blocks.length > 0 ? {
347
+ text: responseText,
348
+ blocks,
349
+ segments,
350
+ metadata,
351
+ updateChunks
352
+ } : {
353
+ text: responseText,
354
+ segments,
355
+ metadata,
356
+ updateChunks
357
+ };
335
358
 
336
359
  const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
337
360
  queries.updateSession(sessionId, { status: 'completed', response: { text: responseText, messageId: assistantMessage.id }, completed_at: Date.now() });
package/static/app.js CHANGED
@@ -525,10 +525,15 @@ class GMGUIApp {
525
525
 
526
526
  looksLikeHtml(text) {
527
527
  const trimmed = text.trim();
528
+ // Check for HTML tags at the start
528
529
  if (/^<[a-z][\s\S]*>/i.test(trimmed)) return true;
529
- if (/<\/(div|span|p|table|ul|ol|h[1-6]|section|article|header|footer|nav|main|aside|details|summary|figure|figcaption|blockquote|pre|code|a|strong|em|img|br|hr)>/i.test(trimmed)) return true;
530
+ // Check for closing tags of common HTML elements
531
+ if (/<\/(div|span|p|table|ul|ol|h[1-6]|section|article|header|footer|nav|main|aside|details|summary|figure|figcaption|blockquote|pre|code|a|strong|em|img|br|hr|button|input|form|label)>/i.test(trimmed)) return true;
532
+ // Check for Tailwind/RippleUI classes (strong indicator of HTML)
533
+ if (/class\s*=\s*["'][^"']*(?:card|alert|badge|btn|table|space-y|p-\d+|text-|bg-|rounded|shadow)/.test(trimmed)) return true;
534
+ // Count HTML tags
530
535
  const tagCount = (trimmed.match(/<[a-z][^>]*>/gi) || []).length;
531
- if (tagCount >= 3) return true;
536
+ if (tagCount >= 2) return true; // Lower threshold for HTML detection
532
537
  return false;
533
538
  }
534
539
 
@@ -599,7 +604,13 @@ class GMGUIApp {
599
604
  el.appendChild(bubble);
600
605
  }
601
606
  } else if (typeof msg.content === 'object' && msg.content !== null) {
602
- if (msg.content.text) {
607
+ // Display segmented content if available
608
+ if (msg.content.segments && Array.isArray(msg.content.segments)) {
609
+ msg.content.segments.forEach(segment => {
610
+ el.appendChild(this.renderSegment(segment));
611
+ });
612
+ } else if (msg.content.text) {
613
+ // Fallback to regular text rendering
603
614
  const parsed = this.parseAndRenderContent(msg.content.text);
604
615
  if (parsed) {
605
616
  parsed.forEach(elem => el.appendChild(elem));
@@ -610,6 +621,8 @@ class GMGUIApp {
610
621
  el.appendChild(bubble);
611
622
  }
612
623
  }
624
+
625
+ // Display blocks if available
613
626
  if (msg.content.blocks && Array.isArray(msg.content.blocks)) {
614
627
  msg.content.blocks.forEach(block => {
615
628
  if (block.type === 'html') {
@@ -621,6 +634,12 @@ class GMGUIApp {
621
634
  }
622
635
  });
623
636
  }
637
+
638
+ // Display metadata if available
639
+ if (msg.content.metadata) {
640
+ const metadataEl = this.renderMetadata(msg.content.metadata);
641
+ if (metadataEl) el.appendChild(metadataEl);
642
+ }
624
643
  } else {
625
644
  const bubble = document.createElement('div');
626
645
  bubble.className = 'message-bubble';
@@ -631,6 +650,127 @@ class GMGUIApp {
631
650
  div.appendChild(el);
632
651
  }
633
652
 
653
+ renderSegment(segment) {
654
+ const el = document.createElement('div');
655
+ el.className = `segment segment-${segment.type}`;
656
+
657
+ if (segment.type === 'code') {
658
+ const pre = document.createElement('pre');
659
+ pre.className = `code-block language-${segment.language || 'text'}`;
660
+ const code = document.createElement('code');
661
+ code.textContent = segment.content;
662
+ pre.appendChild(code);
663
+ el.appendChild(pre);
664
+ } else if (segment.type === 'heading') {
665
+ const tag = `h${Math.min(segment.level, 6)}`;
666
+ const heading = document.createElement(tag);
667
+ heading.className = 'response-heading';
668
+ heading.textContent = segment.content;
669
+ el.appendChild(heading);
670
+ } else if (segment.type === 'blockquote') {
671
+ const quote = document.createElement('blockquote');
672
+ quote.className = 'response-quote';
673
+ quote.textContent = segment.content;
674
+ el.appendChild(quote);
675
+ } else if (segment.type === 'list_item') {
676
+ const li = document.createElement('li');
677
+ li.className = 'response-list-item';
678
+ li.textContent = segment.content;
679
+ el.appendChild(li);
680
+ } else if (segment.type === 'text') {
681
+ const p = document.createElement('p');
682
+ p.className = 'response-text';
683
+ p.innerHTML = segment.content
684
+ .replace(/&/g, '&amp;')
685
+ .replace(/</g, '&lt;')
686
+ .replace(/>/g, '&gt;')
687
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
688
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
689
+ .replace(/`([^`]+)`/g, '<code>$1</code>');
690
+ el.appendChild(p);
691
+ }
692
+
693
+ return el;
694
+ }
695
+
696
+ renderMetadata(metadata) {
697
+ if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
698
+ return null;
699
+ }
700
+
701
+ const container = document.createElement('div');
702
+ container.className = 'response-metadata';
703
+
704
+ if (metadata.tools && metadata.tools.length > 0) {
705
+ const section = document.createElement('div');
706
+ section.className = 'metadata-section tools';
707
+ const title = document.createElement('strong');
708
+ title.textContent = 'Tools Used:';
709
+ section.appendChild(title);
710
+ const ul = document.createElement('ul');
711
+ metadata.tools.forEach(tool => {
712
+ const li = document.createElement('li');
713
+ const code = document.createElement('code');
714
+ code.textContent = tool.name;
715
+ li.appendChild(code);
716
+ if (tool.description) {
717
+ li.appendChild(document.createTextNode(`: ${tool.description}`));
718
+ }
719
+ ul.appendChild(li);
720
+ });
721
+ section.appendChild(ul);
722
+ container.appendChild(section);
723
+ }
724
+
725
+ if (metadata.thinking && metadata.thinking.length > 0) {
726
+ const section = document.createElement('details');
727
+ section.className = 'metadata-section thinking';
728
+ const summary = document.createElement('summary');
729
+ summary.textContent = 'Reasoning';
730
+ section.appendChild(summary);
731
+ metadata.thinking.forEach(thought => {
732
+ const p = document.createElement('p');
733
+ p.textContent = thought;
734
+ section.appendChild(p);
735
+ });
736
+ container.appendChild(section);
737
+ }
738
+
739
+ if (metadata.subagents && metadata.subagents.length > 0) {
740
+ const section = document.createElement('div');
741
+ section.className = 'metadata-section subagents';
742
+ const title = document.createElement('strong');
743
+ title.textContent = 'Subagents:';
744
+ section.appendChild(title);
745
+ const ul = document.createElement('ul');
746
+ metadata.subagents.forEach(agent => {
747
+ const li = document.createElement('li');
748
+ li.textContent = agent;
749
+ ul.appendChild(li);
750
+ });
751
+ section.appendChild(ul);
752
+ container.appendChild(section);
753
+ }
754
+
755
+ if (metadata.tasks && metadata.tasks.length > 0) {
756
+ const section = document.createElement('div');
757
+ section.className = 'metadata-section tasks';
758
+ const title = document.createElement('strong');
759
+ title.textContent = 'Tasks:';
760
+ section.appendChild(title);
761
+ const ul = document.createElement('ul');
762
+ metadata.tasks.forEach(task => {
763
+ const li = document.createElement('li');
764
+ li.textContent = task;
765
+ ul.appendChild(li);
766
+ });
767
+ section.appendChild(ul);
768
+ container.appendChild(section);
769
+ }
770
+
771
+ return container;
772
+ }
773
+
634
774
  async startNewChat(folderPath) {
635
775
  if (!this.selectedAgent) {
636
776
  const firstAgent = Array.from(this.agents.keys())[0];
package/static/styles.css CHANGED
@@ -1418,3 +1418,163 @@ html, body {
1418
1418
  overflow: visible;
1419
1419
  }
1420
1420
  }
1421
+
1422
+ /* Rich Response Rendering */
1423
+
1424
+ /* Code blocks */
1425
+ .segment-code {
1426
+ margin: 1rem 0;
1427
+ }
1428
+
1429
+ .code-block {
1430
+ background: #f5f5f5;
1431
+ border-left: 4px solid #007acc;
1432
+ padding: 1rem;
1433
+ border-radius: 4px;
1434
+ overflow-x: auto;
1435
+ font-family: 'Courier New', monospace;
1436
+ font-size: 0.9rem;
1437
+ line-height: 1.5;
1438
+ color: #333;
1439
+ }
1440
+
1441
+ .code-block code {
1442
+ color: inherit;
1443
+ background: none;
1444
+ padding: 0;
1445
+ }
1446
+
1447
+ /* Inline code */
1448
+ p code {
1449
+ background: #f0f0f0;
1450
+ padding: 0.2em 0.4em;
1451
+ border-radius: 3px;
1452
+ font-family: 'Courier New', monospace;
1453
+ color: #d73a49;
1454
+ font-size: 0.9em;
1455
+ }
1456
+
1457
+ /* Headings */
1458
+ .response-heading {
1459
+ font-weight: 600;
1460
+ margin: 1rem 0 0.5rem 0;
1461
+ line-height: 1.3;
1462
+ }
1463
+
1464
+ .segment-heading {
1465
+ margin-top: 1.5rem;
1466
+ }
1467
+
1468
+ .segment-heading:first-child {
1469
+ margin-top: 0;
1470
+ }
1471
+
1472
+ /* Blockquotes */
1473
+ .response-quote {
1474
+ border-left: 4px solid #ccc;
1475
+ padding-left: 1rem;
1476
+ color: #666;
1477
+ font-style: italic;
1478
+ margin: 0.5rem 0;
1479
+ }
1480
+
1481
+ /* Lists */
1482
+ .response-list-item {
1483
+ margin-left: 2rem;
1484
+ margin-bottom: 0.25rem;
1485
+ }
1486
+
1487
+ /* Text segments */
1488
+ .segment-text {
1489
+ margin: 0.5rem 0;
1490
+ }
1491
+
1492
+ .response-text {
1493
+ margin: 0.5rem 0;
1494
+ line-height: 1.6;
1495
+ color: #333;
1496
+ }
1497
+
1498
+ /* Metadata display */
1499
+ .response-metadata {
1500
+ background: #f9f9f9;
1501
+ border: 1px solid #e0e0e0;
1502
+ border-radius: 8px;
1503
+ padding: 1rem;
1504
+ margin: 1rem 0 0 0;
1505
+ font-size: 0.9rem;
1506
+ }
1507
+
1508
+ .metadata-section {
1509
+ margin-bottom: 1rem;
1510
+ }
1511
+
1512
+ .metadata-section:last-child {
1513
+ margin-bottom: 0;
1514
+ }
1515
+
1516
+ .metadata-section strong {
1517
+ display: block;
1518
+ margin-bottom: 0.5rem;
1519
+ color: #555;
1520
+ font-weight: 600;
1521
+ }
1522
+
1523
+ .metadata-section ul {
1524
+ list-style: none;
1525
+ padding-left: 1rem;
1526
+ margin: 0;
1527
+ }
1528
+
1529
+ .metadata-section li {
1530
+ margin-bottom: 0.25rem;
1531
+ padding-left: 0.5rem;
1532
+ }
1533
+
1534
+ .metadata-section li::before {
1535
+ content: '▸ ';
1536
+ color: #007acc;
1537
+ font-weight: bold;
1538
+ margin-right: 0.5rem;
1539
+ }
1540
+
1541
+ .metadata-section.tools li code {
1542
+ background: #e8f4f8;
1543
+ color: #0071bc;
1544
+ padding: 0.2em 0.4em;
1545
+ border-radius: 3px;
1546
+ }
1547
+
1548
+ .metadata-section.thinking {
1549
+ background: #fff9e6;
1550
+ border: 1px solid #f5e6cc;
1551
+ }
1552
+
1553
+ .metadata-section.thinking summary {
1554
+ cursor: pointer;
1555
+ font-weight: 600;
1556
+ color: #997700;
1557
+ user-select: none;
1558
+ }
1559
+
1560
+ .metadata-section.thinking summary:hover {
1561
+ color: #bb9900;
1562
+ }
1563
+
1564
+ .metadata-section.thinking p {
1565
+ margin: 0.5rem 0;
1566
+ padding-left: 1rem;
1567
+ border-left: 3px solid #f0c674;
1568
+ padding-left: 1rem;
1569
+ color: #666;
1570
+ font-size: 0.9rem;
1571
+ }
1572
+
1573
+ .metadata-section.subagents li::before {
1574
+ content: '🤖 ';
1575
+ }
1576
+
1577
+ .metadata-section.tasks li::before {
1578
+ content: '✓ ';
1579
+ color: #28a745;
1580
+ }