agentgui 1.0.3
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/README.md +213 -0
- package/acp-launcher.js +348 -0
- package/bin/gmgui.cjs +70 -0
- package/database.js +447 -0
- package/install.sh +147 -0
- package/package.json +23 -0
- package/server.js +412 -0
- package/static/app.js +925 -0
- package/static/index.html +201 -0
- package/static/rippleui.css +208 -0
- package/static/styles.css +1432 -0
- package/static/theme.js +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# GMGUI - Multi-Agent ACP Client
|
|
2
|
+
|
|
3
|
+
A buildless, hot-reloading web client for managing multiple Claude Agent Protocol (ACP) agents with real-time communication via WebSocket and MessagePack.
|
|
4
|
+
|
|
5
|
+
**Status**: ✅ Production Ready | **Version**: 1.0.0 | **License**: MIT
|
|
6
|
+
|
|
7
|
+
## Get Started Now - One Command
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bunx agentgui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it. One command starts the server and opens http://localhost:3000/gm/ in your browser.
|
|
14
|
+
|
|
15
|
+
**Works anywhere:** Any system with Bun installed.
|
|
16
|
+
|
|
17
|
+
**Stop anytime:** Press Ctrl+C - clean shutdown.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Multi-Agent Management**: Connect unlimited ACP agents and switch between them instantly
|
|
22
|
+
- **Real-Time Communication**: WebSocket + MessagePack for efficient bidirectional messaging
|
|
23
|
+
- **Desktop Screenshots**: Capture and share desktop screenshots with agents (via scrot)
|
|
24
|
+
- **File Upload/Download**: Upload files for agents to access, download files from conversations
|
|
25
|
+
- **Modern Responsive UI**: Beautiful interface that works on mobile, tablet, and desktop
|
|
26
|
+
- **Conversation History**: Full message history with timestamps
|
|
27
|
+
- **Zero Build Step**: Pure HTML/CSS/JavaScript - no bundling or transpilation
|
|
28
|
+
- **Minimal Dependencies**: Only 1 production dependency (ws)
|
|
29
|
+
|
|
30
|
+
## How It Works
|
|
31
|
+
|
|
32
|
+
**Chat Interface**
|
|
33
|
+
- Real-time message display with timestamps
|
|
34
|
+
- Send/receive messages with agents
|
|
35
|
+
- Clear chat history
|
|
36
|
+
|
|
37
|
+
**File Management**
|
|
38
|
+
- Upload files for agents to access
|
|
39
|
+
- Download files from conversations
|
|
40
|
+
- Files stored automatically
|
|
41
|
+
|
|
42
|
+
**Desktop Sharing**
|
|
43
|
+
- Capture desktop screenshots
|
|
44
|
+
- Share directly with agents
|
|
45
|
+
|
|
46
|
+
**Agent Management**
|
|
47
|
+
- Add agents by ID and endpoint
|
|
48
|
+
- View connection status
|
|
49
|
+
- Switch between agents
|
|
50
|
+
|
|
51
|
+
**Responsive Design**
|
|
52
|
+
- Works on desktop, tablet, and mobile
|
|
53
|
+
- Touch-friendly interface
|
|
54
|
+
- Optimized for all screen sizes
|
|
55
|
+
|
|
56
|
+
## API Endpoints
|
|
57
|
+
|
|
58
|
+
### Get Agents
|
|
59
|
+
```
|
|
60
|
+
GET /api/agents
|
|
61
|
+
```
|
|
62
|
+
Response: `{"agents": [...]}`
|
|
63
|
+
|
|
64
|
+
### Send Message to Agent
|
|
65
|
+
```
|
|
66
|
+
POST /api/agents/{agentId}
|
|
67
|
+
Content-Type: application/json
|
|
68
|
+
|
|
69
|
+
{"type": "message", "content": "..."}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Upload Files
|
|
73
|
+
```
|
|
74
|
+
POST /api/upload
|
|
75
|
+
Content-Type: multipart/form-data
|
|
76
|
+
|
|
77
|
+
file=@path/to/file.txt
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Capture Screenshot
|
|
81
|
+
```
|
|
82
|
+
POST /api/screenshot
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Download File
|
|
86
|
+
```
|
|
87
|
+
GET /uploads/{filename}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
### Environment Variables
|
|
93
|
+
- `PORT` (default: 3000) - Server port
|
|
94
|
+
- `UPLOAD_DIR` (default: /tmp/gmgui-conversations) - File storage location
|
|
95
|
+
|
|
96
|
+
### Data Storage
|
|
97
|
+
|
|
98
|
+
**Conversation History**: Stored in `~/.gmgui/data.db` (hidden folder in your home directory)
|
|
99
|
+
- Uses SQLite database for persistent storage
|
|
100
|
+
- Auto-created on first run with proper permissions
|
|
101
|
+
- Contains conversations, messages, sessions, and event history
|
|
102
|
+
- Data persists across runs and restarts
|
|
103
|
+
- Private to current user (mode 0644)
|
|
104
|
+
|
|
105
|
+
**Browser Local Storage**
|
|
106
|
+
- `gmgui-settings` - User preferences and configuration
|
|
107
|
+
|
|
108
|
+
**Why Hidden Folder?**
|
|
109
|
+
Using `~/.gmgui/` follows Unix conventions:
|
|
110
|
+
- Hidden folders (starting with `.`) keep user directories clean
|
|
111
|
+
- Prevents accidental deletion or modification
|
|
112
|
+
- Private by convention - not visible in casual `ls` output
|
|
113
|
+
- Standard practice for application data (`.config`, `.local`, `.cache`)
|
|
114
|
+
|
|
115
|
+
## Architecture
|
|
116
|
+
|
|
117
|
+
### Server (Node.js)
|
|
118
|
+
- HTTP server with static file serving
|
|
119
|
+
- WebSocket server for agent connections
|
|
120
|
+
- File upload/download endpoints
|
|
121
|
+
- Screenshot capture endpoint
|
|
122
|
+
- Agent management
|
|
123
|
+
|
|
124
|
+
### Client (Browser)
|
|
125
|
+
- Real-time message display
|
|
126
|
+
- File management UI
|
|
127
|
+
- Screenshot capture and preview
|
|
128
|
+
- Agent connection management
|
|
129
|
+
- Settings persistence
|
|
130
|
+
|
|
131
|
+
### File Structure
|
|
132
|
+
```
|
|
133
|
+
gmgui/
|
|
134
|
+
├── server.js # HTTP + WebSocket server
|
|
135
|
+
├── database.js # SQLite persistence
|
|
136
|
+
├── acp-launcher.js # Agent management
|
|
137
|
+
├── bin/gmgui.cjs # npm entry point
|
|
138
|
+
├── static/
|
|
139
|
+
│ ├── index.html # Main UI
|
|
140
|
+
│ ├── app.js # Frontend logic
|
|
141
|
+
│ ├── styles.css # Responsive styles
|
|
142
|
+
│ ├── theme.js # Theme management
|
|
143
|
+
│ └── rippleui.css # CSS framework
|
|
144
|
+
├── install.sh # One-liner installer
|
|
145
|
+
├── package.json # Dependencies
|
|
146
|
+
└── README.md # This file
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
### Enable Hot Reload (during development)
|
|
152
|
+
```bash
|
|
153
|
+
npm run dev
|
|
154
|
+
```
|
|
155
|
+
Changes to `static/` files auto-refresh the browser.
|
|
156
|
+
|
|
157
|
+
## Browser Support
|
|
158
|
+
|
|
159
|
+
Works on all modern browsers:
|
|
160
|
+
- Chrome/Edge 63+
|
|
161
|
+
- Firefox 55+
|
|
162
|
+
- Safari 11+
|
|
163
|
+
- Mobile browsers (iOS Safari, Chrome Mobile, etc.)
|
|
164
|
+
|
|
165
|
+
## Performance
|
|
166
|
+
|
|
167
|
+
- **Fast Startup**: ~100ms with Bun
|
|
168
|
+
- **No Build Step**: Source code runs directly
|
|
169
|
+
- **Efficient Messaging**: MessagePack reduces payload size by 50%
|
|
170
|
+
- **Real-time Updates**: <50ms WebSocket latency
|
|
171
|
+
- **Memory Efficient**: ~20MB typical usage
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
**Port Already in Use**
|
|
176
|
+
```bash
|
|
177
|
+
PORT=3001 bunx gmgui
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Agent Won't Connect**
|
|
181
|
+
- Verify agent endpoint is accessible
|
|
182
|
+
- Check browser console for errors
|
|
183
|
+
- Ensure agent is sending valid ACP messages
|
|
184
|
+
|
|
185
|
+
**Files Not Uploading**
|
|
186
|
+
- Check browser console for errors
|
|
187
|
+
- Verify sufficient disk space available
|
|
188
|
+
|
|
189
|
+
## Security
|
|
190
|
+
|
|
191
|
+
- Path traversal protection on file uploads
|
|
192
|
+
- WebSocket message validation
|
|
193
|
+
- File upload restrictions
|
|
194
|
+
- No sensitive data in logs
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT - Free to use, modify, and distribute
|
|
199
|
+
|
|
200
|
+
## Need Help?
|
|
201
|
+
|
|
202
|
+
Open an issue on GitHub: https://github.com/AnEntrypoint/gmgui/issues
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
**Ready to manage multiple ACP agents?** Run this now:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
curl -fsSL https://raw.githubusercontent.com/AnEntrypoint/gmgui/main/install.sh | bash
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Then open http://localhost:3000/gm/ in your browser
|
|
213
|
+
# Triggered npm publishing
|
package/acp-launcher.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
|
|
4
|
+
export default class ACPConnection {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.child = null;
|
|
7
|
+
this.buffer = '';
|
|
8
|
+
this.nextRequestId = 1;
|
|
9
|
+
this.pendingRequests = new Map();
|
|
10
|
+
this.sessionId = null;
|
|
11
|
+
this.onUpdate = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async connect(agentType, cwd) {
|
|
15
|
+
const env = { ...process.env };
|
|
16
|
+
delete env.NODE_OPTIONS;
|
|
17
|
+
delete env.NODE_INSPECT;
|
|
18
|
+
delete env.NODE_DEBUG;
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let spawned = false;
|
|
22
|
+
try {
|
|
23
|
+
if (agentType === 'opencode') {
|
|
24
|
+
this.child = spawn('opencode', ['acp'], { cwd, stdio: ['pipe', 'pipe', 'pipe'], env, shell: false });
|
|
25
|
+
} else {
|
|
26
|
+
this.child = spawn('claude-code-acp', [], { cwd, stdio: ['pipe', 'pipe', 'pipe'], env, shell: false });
|
|
27
|
+
}
|
|
28
|
+
spawned = true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
reject(new Error(`Failed to spawn ACP process (${agentType}): ${err.message}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const timeoutId = setTimeout(() => {
|
|
35
|
+
reject(new Error(`ACP process (${agentType}) failed to initialize within 2 seconds`));
|
|
36
|
+
}, 2000);
|
|
37
|
+
|
|
38
|
+
this.child.stderr.on('data', d => console.error(`[ACP:${agentType}:stderr]`, d.toString().trim()));
|
|
39
|
+
this.child.on('error', err => {
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
console.error(`[ACP:${agentType}:error]`, err.message);
|
|
42
|
+
reject(new Error(`ACP process error (${agentType}): ${err.message}`));
|
|
43
|
+
});
|
|
44
|
+
this.child.on('exit', (code, signal) => {
|
|
45
|
+
clearTimeout(timeoutId);
|
|
46
|
+
console.log(`[ACP:${agentType}] exited code=${code} signal=${signal}`);
|
|
47
|
+
this.child = null;
|
|
48
|
+
for (const [id, req] of this.pendingRequests) {
|
|
49
|
+
req.reject(new Error('ACP process exited'));
|
|
50
|
+
clearTimeout(req.timeoutId);
|
|
51
|
+
}
|
|
52
|
+
this.pendingRequests.clear();
|
|
53
|
+
if (!spawned) {
|
|
54
|
+
reject(new Error(`ACP process (${agentType}) exited before connection established`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.child.stdout.setEncoding('utf8');
|
|
59
|
+
this.child.stdout.on('data', data => {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
this.buffer += data;
|
|
62
|
+
const lines = this.buffer.split('\n');
|
|
63
|
+
this.buffer = lines.pop() || '';
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
if (!line.trim()) continue;
|
|
66
|
+
try {
|
|
67
|
+
this.handleMessage(JSON.parse(line));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('[ACP:parse]', line.substring(0, 200), e.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setTimeout(() => resolve(), 500);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
handleMessage(msg) {
|
|
79
|
+
if (msg.method) {
|
|
80
|
+
this.handleIncoming(msg);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
|
|
84
|
+
const req = this.pendingRequests.get(msg.id);
|
|
85
|
+
this.pendingRequests.delete(msg.id);
|
|
86
|
+
clearTimeout(req.timeoutId);
|
|
87
|
+
if (msg.error) {
|
|
88
|
+
req.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
89
|
+
} else {
|
|
90
|
+
req.resolve(msg.result);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleIncoming(msg) {
|
|
96
|
+
if (msg.method === 'session/update' && msg.params) {
|
|
97
|
+
if (this.onUpdate) this.onUpdate(msg.params);
|
|
98
|
+
this.resetPromptTimeout();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (msg.method === 'session/request_permission' && msg.id !== undefined) {
|
|
102
|
+
this.sendResponse(msg.id, { outcome: { outcome: 'selected', optionId: 'allow' } });
|
|
103
|
+
this.resetPromptTimeout();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (msg.method === 'fs/read_text_file' && msg.id !== undefined) {
|
|
107
|
+
const filePath = msg.params?.path;
|
|
108
|
+
try {
|
|
109
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
110
|
+
this.sendResponse(msg.id, { content });
|
|
111
|
+
} catch (e) {
|
|
112
|
+
this.sendError(msg.id, -32000, e.message);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (msg.method === 'fs/write_text_file' && msg.id !== undefined) {
|
|
117
|
+
const { path: filePath, content } = msg.params || {};
|
|
118
|
+
try {
|
|
119
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
120
|
+
this.sendResponse(msg.id, null);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
this.sendError(msg.id, -32000, e.message);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resetPromptTimeout() {
|
|
129
|
+
for (const [id, req] of this.pendingRequests) {
|
|
130
|
+
if (req.method === 'session/prompt') {
|
|
131
|
+
clearTimeout(req.timeoutId);
|
|
132
|
+
req.timeoutId = setTimeout(() => {
|
|
133
|
+
this.pendingRequests.delete(id);
|
|
134
|
+
req.reject(new Error('session/prompt timeout'));
|
|
135
|
+
}, 300000);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sendRequest(method, params, timeoutMs = 60000) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
if (!this.child) { reject(new Error('ACP not connected')); return; }
|
|
143
|
+
const id = this.nextRequestId++;
|
|
144
|
+
const timeoutId = setTimeout(() => {
|
|
145
|
+
this.pendingRequests.delete(id);
|
|
146
|
+
reject(new Error(`${method} timeout (${timeoutMs}ms)`));
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
this.pendingRequests.set(id, { resolve, reject, timeoutId, method });
|
|
149
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, ...(params && { params }) }) + '\n');
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
sendResponse(id, result) {
|
|
154
|
+
if (!this.child) return;
|
|
155
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sendError(id, code, message) {
|
|
159
|
+
if (!this.child) return;
|
|
160
|
+
this.child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async initialize() {
|
|
164
|
+
return this.sendRequest('initialize', {
|
|
165
|
+
protocolVersion: 1,
|
|
166
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async newSession(cwd) {
|
|
171
|
+
const result = await this.sendRequest('session/new', { cwd, mcpServers: [] }, 120000);
|
|
172
|
+
this.sessionId = result.sessionId;
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async setSessionMode(modeId) {
|
|
177
|
+
return this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async injectSkills(skills) {
|
|
181
|
+
const skillDescriptions = {
|
|
182
|
+
'html_rendering': {
|
|
183
|
+
name: 'HTML Rendering',
|
|
184
|
+
description: 'Render styled HTML blocks directly in the chat interface',
|
|
185
|
+
capability: 'Send a sessionUpdate with this exact format:\n{\n "sessionUpdate": "html_content",\n "content": {\n "html": "<div style=\\"padding:1rem; border:1px solid #ccc; border-radius:0.5rem; background:#f9f9f9\\">Your HTML content here</div>",\n "title": "Optional title for the block",\n "id": "optional-unique-id"\n }\n}\nThe HTML will render as a styled square/box block in the conversation.'
|
|
186
|
+
},
|
|
187
|
+
'image_display': {
|
|
188
|
+
name: 'Image Display',
|
|
189
|
+
description: 'Display images from the filesystem in styled blocks',
|
|
190
|
+
capability: 'Send a sessionUpdate with this exact format:\n{\n "sessionUpdate": "image_content",\n "content": {\n "path": "/path/to/image.png",\n "title": "Optional image title",\n "alt": "Alt text for accessibility"\n }\n}\nSupported formats: PNG, JPEG, GIF, WebP, SVG. The image will render in a styled block.'
|
|
191
|
+
},
|
|
192
|
+
'scrot': {
|
|
193
|
+
name: 'Screenshot Utility',
|
|
194
|
+
description: 'Capture screenshots of the desktop or specific windows',
|
|
195
|
+
capability: 'Use scrot command-line tool to capture and save images to filesystem, then display using image_display skill'
|
|
196
|
+
},
|
|
197
|
+
'fs_access': {
|
|
198
|
+
name: 'Filesystem Access',
|
|
199
|
+
description: 'Read and write files, browse directories',
|
|
200
|
+
capability: 'Full read/write access to user home directory and workspace'
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const skillsToInject = skills
|
|
205
|
+
.map(s => skillDescriptions[s])
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
|
|
208
|
+
if (skillsToInject.length === 0) return;
|
|
209
|
+
|
|
210
|
+
const prompt = [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
text: `CRITICAL: HTML-FIRST VISUAL THINKING & COMMUNICATION MANDATE
|
|
214
|
+
|
|
215
|
+
YOU MUST EXPRESS YOUR ENTIRE THOUGHT PROCESS EXCLUSIVELY THROUGH HTML BLOCKS.
|
|
216
|
+
PLAIN TEXT EXPLANATIONS ARE FORBIDDEN - REPLACE ALL TEXT WITH VISUAL HTML.
|
|
217
|
+
|
|
218
|
+
ABSOLUTE REQUIREMENTS:
|
|
219
|
+
1. EVERY response must be 100% visual HTML-based communication
|
|
220
|
+
2. All thinking, analysis, and explanations MUST be rendered as styled HTML blocks
|
|
221
|
+
3. Plain text responses will be treated as incomplete work
|
|
222
|
+
4. Each HTML block must have clear visual indicators of its purpose and status
|
|
223
|
+
5. Structure your thinking visually using progressive disclosure patterns
|
|
224
|
+
|
|
225
|
+
THOUGHT PROCESS VISUALIZATION PATTERNS:
|
|
226
|
+
|
|
227
|
+
═══ PATTERN 1: STEP-BY-STEP ANALYSIS WITH NUMBERED VISUAL BLOCKS ═══
|
|
228
|
+
Use numbered visual blocks to show sequential thinking:
|
|
229
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border-left:4px solid #3b82f6;background:#eff6ff;border-radius:0.5rem'><div style='display:flex;gap:1rem;margin-bottom:0.75rem'><div style='font-weight:bold;color:#1e40af;background:#dbeafe;padding:0.25rem 0.75rem;border-radius:0.25rem;min-width:3rem;text-align:center'>STEP 1</div><div style='flex:1'><strong>Understanding the Problem</strong><br/>First, we analyze what we're dealing with...</div></div></div>", "title": "Analysis Progress"}}
|
|
230
|
+
|
|
231
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border-left:4px solid #8b5cf6;background:#faf5ff;border-radius:0.5rem'><div style='display:flex;gap:1rem;margin-bottom:0.75rem'><div style='font-weight:bold;color:#5b21b6;background:#ede9fe;padding:0.25rem 0.75rem;border-radius:0.25rem;min-width:3rem;text-align:center'>STEP 2</div><div style='flex:1'><strong>Exploring Options</strong><br/>Consider these approaches...</div></div></div>", "title": "Analysis Progress"}}
|
|
232
|
+
|
|
233
|
+
═══ PATTERN 2: DECISION TREE WITH BRANCHING VISUAL STRUCTURE ═══
|
|
234
|
+
Show branching logic and decision paths:
|
|
235
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb;font-family:monospace'><div style='margin-bottom:1rem'><div style='font-weight:bold;color:#1f2937'>Root Decision</div><div style='margin-left:1rem;margin-top:0.5rem;padding-left:1rem;border-left:2px solid #d1d5db'><div style='color:#059669;font-weight:bold'>✓ IF condition A ➜ Path 1</div><div style='color:#dc2626;font-weight:bold'>✗ ELSE ➜ Path 2</div></div></div></div>", "title": "Decision Logic"}}
|
|
236
|
+
|
|
237
|
+
═══ PATTERN 3: PROGRESS INDICATOR WITH VISUAL STATUS ═══
|
|
238
|
+
Show completion and progress visually:
|
|
239
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><div style='margin-bottom:1rem'><div style='display:flex;gap:0.5rem;margin-bottom:0.5rem'><span style='color:#059669;font-weight:bold'>✓ THINKING</span><span style='color:#059669;font-weight:bold'>✓ ANALYZING</span><span style='color:#f59e0b;font-weight:bold'>◐ DETERMINING</span><span style='color:#d1d5db;font-weight:bold'>○ IMPLEMENTING</span></div><div style='width:100%;height:0.5rem;background:#e5e7eb;border-radius:0.25rem;overflow:hidden'><div style='width:75%;height:100%;background:#3b82f6'></div></div><div style='text-align:right;font-size:0.875rem;color:#6b7280'>75% complete</div></div></div>", "title": "Thought Process Status"}}
|
|
240
|
+
|
|
241
|
+
═══ PATTERN 4: EXPANDABLE/COLLAPSIBLE REASONING SECTIONS ═══
|
|
242
|
+
Structure nested thinking with visual hierarchy:
|
|
243
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><div style='cursor:pointer;user-select:none;margin-bottom:0.75rem'><div style='font-weight:bold;color:#1f2937;display:flex;align-items:center;gap:0.5rem'><span style='display:inline-block;width:1.5rem'>▶ REASONING:</span><span>Why this approach works</span></div></div><div style='margin-left:1rem;padding:0.75rem;background:#f3f4f6;border-left:2px solid #9ca3af;border-radius:0.25rem'><div>Key insight: The most direct path minimizes complexity...</div></div></div>", "title": "Detailed Analysis"}}
|
|
244
|
+
|
|
245
|
+
═══ PATTERN 5: COLOR-CODED STATUS INDICATORS ═══
|
|
246
|
+
Use colors to indicate thinking state and conclusions:
|
|
247
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='display:grid;grid-template-columns:repeat(4,1fr);gap:0.75rem;padding:1.5rem'><div style='padding:1rem;border-radius:0.5rem;background:#dbeafe;border:2px solid #0ea5e9;text-align:center'><div style='font-weight:bold;color:#0c4a6e;font-size:0.875rem'>THINKING</div><div style='color:#0c4a6e;margin-top:0.5rem'>🧠</div></div><div style='padding:1rem;border-radius:0.5rem;background:#fef3c7;border:2px solid #fbbf24;text-align:center'><div style='font-weight:bold;color:#78350f;font-size:0.875rem'>ANALYZING</div><div style='color:#78350f;margin-top:0.5rem'>🔍</div></div><div style='padding:1rem;border-radius:0.5rem;background:#dcfce7;border:2px solid #22c55e;text-align:center'><div style='font-weight:bold;color:#15803d;font-size:0.875rem'>DONE</div><div style='color:#15803d;margin-top:0.5rem'>✓</div></div><div style='padding:1rem;border-radius:0.5rem;background:#fee2e2;border:2px solid #ef4444;text-align:center'><div style='font-weight:bold;color:#7f1d1d;font-size:0.875rem'>BLOCKED</div><div style='color:#7f1d1d;margin-top:0.5rem'>⚠</div></div></div>", "title": "Status Indicators"}}
|
|
248
|
+
|
|
249
|
+
═══ PRACTICAL EXAMPLES: VISUALIZE YOUR THINKING ═══
|
|
250
|
+
|
|
251
|
+
EXAMPLE 1: Problem Analysis Visualization
|
|
252
|
+
Instead of: "I need to analyze this problem in parts"
|
|
253
|
+
Do this:
|
|
254
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><h3 style='margin-top:0;color:#1f2937'>Problem Analysis</h3><div style='margin-top:1rem'><div style='padding:0.75rem;background:#dbeafe;border-left:4px solid #0ea5e9;margin-bottom:0.5rem;border-radius:0.25rem'><strong>Part 1:</strong> Context and constraints</div><div style='padding:0.75rem;background:#dbeafe;border-left:4px solid #0ea5e9;margin-bottom:0.5rem;border-radius:0.25rem'><strong>Part 2:</strong> Key variables and dependencies</div><div style='padding:0.75rem;background:#dbeafe;border-left:4px solid #0ea5e9;border-radius:0.25rem'><strong>Part 3:</strong> Potential failure points</div></div></div>", "title": "Analysis Breakdown"}}
|
|
255
|
+
|
|
256
|
+
EXAMPLE 2: Decision Making Process
|
|
257
|
+
Instead of: "Let me think about the options"
|
|
258
|
+
Do this:
|
|
259
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><h3 style='margin-top:0;color:#1f2937'>Decision Matrix</h3><table style='width:100%;border-collapse:collapse;margin-top:1rem'><tr style='background:#f3f4f6'><th style='border:1px solid #e5e7eb;padding:0.75rem;text-align:left'>Option</th><th style='border:1px solid #e5e7eb;padding:0.75rem'>Pros</th><th style='border:1px solid #e5e7eb;padding:0.75rem'>Cons</th><th style='border:1px solid #e5e7eb;padding:0.75rem'>Score</th></tr><tr><td style='border:1px solid #e5e7eb;padding:0.75rem'>Option A</td><td style='border:1px solid #e5e7eb;padding:0.75rem;color:#059669'>Fast, simple</td><td style='border:1px solid #e5e7eb;padding:0.75rem;color:#dc2626'>Limited scope</td><td style='border:1px solid #e5e7eb;padding:0.75rem;font-weight:bold'>7/10</td></tr><tr><td style='border:1px solid #e5e7eb;padding:0.75rem'>Option B</td><td style='border:1px solid #e5e7eb;padding:0.75rem;color:#059669'>Comprehensive</td><td style='border:1px solid #e5e7eb;padding:0.75rem;color:#dc2626'>More complex</td><td style='border:1px solid #e5e7eb;padding:0.75rem;font-weight:bold'>9/10</td></tr></table></div>", "title": "Options Evaluation"}}
|
|
260
|
+
|
|
261
|
+
EXAMPLE 3: Logical Reasoning Flow
|
|
262
|
+
Instead of: "Here's my reasoning..."
|
|
263
|
+
Do this:
|
|
264
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><h3 style='margin-top:0;color:#1f2937'>Reasoning Chain</h3><div style='margin-top:1rem'><div style='display:flex;align-items:center;margin-bottom:1rem'><div style='background:#dbeafe;border-radius:50%;width:2rem;height:2rem;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#0c4a6e;flex-shrink:0'>1</div><div style='margin-left:1rem;flex:1'>Observation: The system shows pattern X</div></div><div style='margin-left:1rem;border-left:2px solid #0ea5e9;height:1rem'></div><div style='display:flex;align-items:center;margin-bottom:1rem'><div style='background:#fef3c7;border-radius:50%;width:2rem;height:2rem;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#78350f;flex-shrink:0;margin-left:1rem'>2</div><div style='margin-left:1rem;flex:1'>Analysis: X implies Y based on principle Z</div></div><div style='margin-left:1rem;border-left:2px solid #fbbf24;height:1rem'></div><div style='display:flex;align-items:center'><div style='background:#dcfce7;border-radius:50%;width:2rem;height:2rem;display:flex;align-items:center;justify-content:center;font-weight:bold;color:#15803d;flex-shrink:0;margin-left:1rem'>3</div><div style='margin-left:1rem;flex:1'>Conclusion: Therefore, approach A is optimal</div></div></div></div>", "title": "Logical Flow"}}
|
|
265
|
+
|
|
266
|
+
EXAMPLE 4: Solution Alternatives with Confidence
|
|
267
|
+
Instead of: "There are different ways to solve this"
|
|
268
|
+
Do this:
|
|
269
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border:1px solid #e5e7eb;border-radius:0.5rem;background:#f9fafb'><h3 style='margin-top:0;color:#1f2937'>Solution Alternatives</h3><div style='margin-top:1rem;display:grid;gap:1rem'><div style='padding:1rem;background:#dcfce7;border:2px solid #22c55e;border-radius:0.5rem'><div style='font-weight:bold;color:#15803d'>Solution A: Direct Implementation</div><div style='margin-top:0.5rem;font-size:0.875rem'>Confidence: <span style='color:#15803d;font-weight:bold'>95%</span></div></div><div style='padding:1rem;background:#fef3c7;border:2px solid #fbbf24;border-radius:0.5rem'><div style='font-weight:bold;color:#78350f'>Solution B: Iterative Approach</div><div style='margin-top:0.5rem;font-size:0.875rem'>Confidence: <span style='color:#78350f;font-weight:bold'>75%</span></div></div><div style='padding:1rem;background:#fee2e2;border:2px solid #ef4444;border-radius:0.5rem'><div style='font-weight:bold;color:#7f1d1d'>Solution C: Experimental Method</div><div style='margin-top:0.5rem;font-size:0.875rem'>Confidence: <span style='color:#7f1d1d;font-weight:bold'>50%</span></div></div></div></div>", "title": "Alternative Approaches"}}
|
|
270
|
+
|
|
271
|
+
EXAMPLE 5: Final Conclusion with Confidence Indicator
|
|
272
|
+
Instead of: "In conclusion..."
|
|
273
|
+
Do this:
|
|
274
|
+
{"sessionUpdate": "html_content", "content": {"html": "<div style='padding:1.5rem;border-left:6px solid #059669;background:#f0fdf4;border-radius:0.5rem'><h3 style='margin-top:0;color:#15803d;display:flex;align-items:center;gap:0.5rem'><span style='font-size:1.5em'>✓</span>Final Conclusion</h3><div style='margin-top:0.75rem;color:#166534'><strong>Primary Finding:</strong> The recommended approach is X because of reasons A, B, and C.</div><div style='margin-top:0.75rem'><div style='display:flex;align-items:center;gap:0.75rem'><span style='font-weight:bold'>Confidence Level:</span><div style='flex:1;height:1rem;background:#d1d5db;border-radius:0.25rem;overflow:hidden'><div style='width:92%;height:100%;background:#10b981'></div></div><span style='font-weight:bold'>92%</span></div></div></div>", "title": "Conclusion"}}
|
|
275
|
+
|
|
276
|
+
═══ ESSENTIAL GUIDELINES ═══
|
|
277
|
+
|
|
278
|
+
VISUAL HIERARCHY:
|
|
279
|
+
- Use size, color, and spacing to guide attention
|
|
280
|
+
- Most important insights get the largest/brightest blocks
|
|
281
|
+
- Supporting details in smaller, lighter blocks
|
|
282
|
+
- Use section headers to organize complex thinking
|
|
283
|
+
|
|
284
|
+
ICONS & SYMBOLS (Use these for visual clarity):
|
|
285
|
+
- ✓ = Complete, correct, confirmed
|
|
286
|
+
- ✗ = Incomplete, incorrect, rejected
|
|
287
|
+
- ◐ = In progress, partial
|
|
288
|
+
- ○ = Pending, not started
|
|
289
|
+
- → = Implies, leads to, flows to
|
|
290
|
+
- ⚠ = Warning, caution, issue
|
|
291
|
+
- 🧠 = Thinking, analyzing
|
|
292
|
+
- 🔍 = Investigating, examining
|
|
293
|
+
- 📊 = Data, metrics, analysis
|
|
294
|
+
|
|
295
|
+
STYLING RULES:
|
|
296
|
+
- Every block must have: padding, border, border-radius, background color
|
|
297
|
+
- Use consistent color scheme: blue for thinking, yellow for analysis, green for complete, red for blocked
|
|
298
|
+
- Never use plain white backgrounds - use light grays (#f9fafb, #f3f4f6)
|
|
299
|
+
- Minimum border: 1px solid #e5e7eb
|
|
300
|
+
- Minimum padding: 1.5rem for block containers
|
|
301
|
+
|
|
302
|
+
RippleUI COMPATIBILITY:
|
|
303
|
+
When possible, use RippleUI classes instead of inline styles:
|
|
304
|
+
- Color classes: bg-primary, bg-secondary, text-primary, text-secondary
|
|
305
|
+
- Spacing: p-4, p-6, m-2, gap-3
|
|
306
|
+
- Borders: border-color, rounded-lg
|
|
307
|
+
- But inline styles are acceptable when needed for dynamic values
|
|
308
|
+
|
|
309
|
+
MULTI-BLOCK FLOW:
|
|
310
|
+
Send separate sessionUpdate calls for each visual block:
|
|
311
|
+
- First block: Analysis/problem statement
|
|
312
|
+
- Middle blocks: Reasoning, options, decision logic
|
|
313
|
+
- Final block: Conclusion with confidence
|
|
314
|
+
|
|
315
|
+
NO PLAIN TEXT:
|
|
316
|
+
- Do not explain your thinking in regular text messages
|
|
317
|
+
- All explanations must be in HTML blocks
|
|
318
|
+
- Plain text is for direct command responses only (like "Done" or error messages)
|
|
319
|
+
- Any substantive communication MUST be visual HTML
|
|
320
|
+
|
|
321
|
+
Available skills: ${skillsToInject.map(s => s.name).join(', ')}`
|
|
322
|
+
}
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
return this.sendRequest('session/skill_inject', {
|
|
326
|
+
sessionId: this.sessionId,
|
|
327
|
+
skills: skillsToInject,
|
|
328
|
+
notification: prompt
|
|
329
|
+
}).catch(() => null);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async sendPrompt(prompt) {
|
|
333
|
+
const promptContent = Array.isArray(prompt) ? prompt : [{ type: 'text', text: prompt }];
|
|
334
|
+
return this.sendRequest('session/prompt', { sessionId: this.sessionId, prompt: promptContent }, 300000);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
isRunning() {
|
|
338
|
+
return this.child && !this.child.killed;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async terminate() {
|
|
342
|
+
if (!this.child) return;
|
|
343
|
+
this.child.stdin.end();
|
|
344
|
+
this.child.kill('SIGTERM');
|
|
345
|
+
await new Promise(r => { this.child?.on('exit', r); setTimeout(r, 5000); });
|
|
346
|
+
this.child = null;
|
|
347
|
+
}
|
|
348
|
+
}
|
package/bin/gmgui.cjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn, spawnSync } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const projectRoot = path.join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
function hasBun() {
|
|
9
|
+
try {
|
|
10
|
+
spawnSync('which', ['bun'], { stdio: 'pipe' });
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function gmgui(args = []) {
|
|
18
|
+
const command = args[0] || 'start';
|
|
19
|
+
|
|
20
|
+
if (command === 'start') {
|
|
21
|
+
const useBun = hasBun();
|
|
22
|
+
const installer = useBun ? 'bun' : 'npm';
|
|
23
|
+
|
|
24
|
+
// Ensure dependencies are installed
|
|
25
|
+
const nodeModulesPath = path.join(projectRoot, 'node_modules');
|
|
26
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
27
|
+
console.log(`Installing dependencies with ${installer}...`);
|
|
28
|
+
const installResult = spawnSync(installer, ['install'], {
|
|
29
|
+
cwd: projectRoot,
|
|
30
|
+
stdio: 'inherit'
|
|
31
|
+
});
|
|
32
|
+
if (installResult.status !== 0) {
|
|
33
|
+
throw new Error(`${installer} install failed with code ${installResult.status}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const port = process.env.PORT || 3000;
|
|
38
|
+
const baseUrl = process.env.BASE_URL || '/gm';
|
|
39
|
+
const runtime = useBun ? 'bun' : 'node';
|
|
40
|
+
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const ps = spawn(runtime, [path.join(projectRoot, 'server.js')], {
|
|
43
|
+
cwd: projectRoot,
|
|
44
|
+
env: { ...process.env, PORT: port, BASE_URL: baseUrl },
|
|
45
|
+
stdio: 'inherit'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
ps.on('exit', (code) => {
|
|
49
|
+
if (code === 0) resolve();
|
|
50
|
+
else reject(new Error(`Server exited with code ${code}`));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ps.on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
} else {
|
|
56
|
+
throw new Error(`Unknown command: ${command}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run if this file is executed directly (works with symlinks, npm, npx)
|
|
61
|
+
const isBinFile = process.argv[1].endsWith('gmgui.cjs') ||
|
|
62
|
+
process.argv[1].endsWith('gmgui.js') ||
|
|
63
|
+
process.argv[1].endsWith('/gmgui') ||
|
|
64
|
+
process.argv[1].includes('bin/gmgui');
|
|
65
|
+
if (isBinFile) {
|
|
66
|
+
gmgui(process.argv.slice(2)).catch(err => {
|
|
67
|
+
console.error(err.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
}
|