agentgui 1.0.15 → 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 +93 -19
- package/hot-reload-manager.js +186 -0
- package/package.json +1 -1
- package/server.js +3 -1
- package/static/app.js +7 -2
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.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
package/server.js
CHANGED
|
@@ -32,7 +32,9 @@ async function getACP(agentId, cwd) {
|
|
|
32
32
|
await conn.initialize();
|
|
33
33
|
await conn.newSession(cwd);
|
|
34
34
|
await conn.setSessionMode('bypassPermissions');
|
|
35
|
-
|
|
35
|
+
// Inject system prompt to ensure HTML/RippleUI formatting
|
|
36
|
+
await conn.injectSkills();
|
|
37
|
+
await conn.injectSystemContext();
|
|
36
38
|
acpPool.set(agentId, conn);
|
|
37
39
|
console.log(`ACP connection ready for ${agentId} in ${cwd}`);
|
|
38
40
|
return conn;
|
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
|
-
|
|
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 >=
|
|
536
|
+
if (tagCount >= 2) return true; // Lower threshold for HTML detection
|
|
532
537
|
return false;
|
|
533
538
|
}
|
|
534
539
|
|