agentgui 1.0.17 → 1.0.19
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/IMPLEMENTATION_STATUS.md +189 -0
- package/RESPONSE_ISSUES.md +157 -0
- package/html-wrapper.js +117 -0
- package/package.json +1 -1
- package/response-formatter.js +84 -40
- package/server.js +12 -3
- package/static/app.js +59 -0
- package/static/styles.css +112 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# AgentGUI Implementation Status
|
|
2
|
+
|
|
3
|
+
## ✅ Completed Features
|
|
4
|
+
|
|
5
|
+
### 1. OAuth Authentication
|
|
6
|
+
- ✅ Binary discovery for `claude-code-acp`
|
|
7
|
+
- ✅ Automatic PATH management
|
|
8
|
+
- ✅ Timeout optimization for ACP bridge
|
|
9
|
+
- ✅ Uses local Claude Code credentials (no API key needed)
|
|
10
|
+
|
|
11
|
+
### 2. Response Formatting Infrastructure
|
|
12
|
+
- ✅ ResponseFormatter module for parsing responses
|
|
13
|
+
- ✅ Segment detection (code, headings, text, lists)
|
|
14
|
+
- ✅ Metadata extraction (tools, thinking, tasks, subagents)
|
|
15
|
+
- ✅ Frontend rendering for segments and metadata
|
|
16
|
+
|
|
17
|
+
### 3. HTML/RippleUI System
|
|
18
|
+
- ✅ Enhanced system prompt with detailed HTML instructions
|
|
19
|
+
- ✅ HTMLWrapper module for automatic HTML wrapping
|
|
20
|
+
- ✅ Markdown parsing to HTML conversion
|
|
21
|
+
- ✅ Tailwind CSS styling integration
|
|
22
|
+
|
|
23
|
+
### 4. Frontend Improvements
|
|
24
|
+
- ✅ Enhanced HTML detection (tags + Tailwind classes)
|
|
25
|
+
- ✅ Rich CSS styling for code blocks, metadata, segments
|
|
26
|
+
- ✅ Responsive design for all components
|
|
27
|
+
- ✅ Print-friendly styles
|
|
28
|
+
|
|
29
|
+
### 5. Infrastructure
|
|
30
|
+
- ✅ Hot reload preparation (HotReloadManager module)
|
|
31
|
+
- ✅ Git version control with comprehensive commit history
|
|
32
|
+
- ✅ Port configuration (3000 dev, 9897 production)
|
|
33
|
+
- ✅ Database persistence
|
|
34
|
+
|
|
35
|
+
## 🔄 Partially Implemented
|
|
36
|
+
|
|
37
|
+
### Hot Reload for Node Modules
|
|
38
|
+
- ⚠️ Static files auto-reload: YES (CSS, HTML, JS in browser)
|
|
39
|
+
- ⚠️ Node.js module changes: NO (requires server restart)
|
|
40
|
+
- **Workaround**: Changes to `.js` files in `/config/workspace/agentgui/` require manual server restart
|
|
41
|
+
- **Future**: Implement full ES module reloading
|
|
42
|
+
|
|
43
|
+
## 📊 Current Architecture
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
User → Browser (9897)
|
|
47
|
+
↓
|
|
48
|
+
Server.js (Node.js)
|
|
49
|
+
├→ ACP Pool (connects to claude-code-acp)
|
|
50
|
+
│ └→ OAuth via local credentials
|
|
51
|
+
├→ HTMLWrapper (wraps responses in HTML)
|
|
52
|
+
├→ ResponseFormatter (segments & metadata)
|
|
53
|
+
└→ Database (SQLite)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 🎯 Current Limitations
|
|
57
|
+
|
|
58
|
+
1. **System Prompt Not Fully Enforced**
|
|
59
|
+
- Claude Code's system prompt about HTML responses works partially
|
|
60
|
+
- Plain text responses are now auto-wrapped by HTMLWrapper
|
|
61
|
+
- Result: All responses display as HTML regardless of original format
|
|
62
|
+
|
|
63
|
+
2. **Hot Module Reloading**
|
|
64
|
+
- Static files (CSS, HTML) reload automatically
|
|
65
|
+
- JavaScript/Node modules need manual restart
|
|
66
|
+
- Recommendation: Changes to server logic need restart
|
|
67
|
+
|
|
68
|
+
3. **ACP Skill Injection**
|
|
69
|
+
- `session/skill_inject` not supported by current ACP version
|
|
70
|
+
- Falls back gracefully without error
|
|
71
|
+
- System prompt still injected via context
|
|
72
|
+
|
|
73
|
+
## 📋 Next Steps
|
|
74
|
+
|
|
75
|
+
### For Full HTML Response Enforcement
|
|
76
|
+
1. ✅ Already Done: HTMLWrapper auto-converts plain text to HTML
|
|
77
|
+
2. No further action needed - all responses now display as beautifully formatted HTML
|
|
78
|
+
|
|
79
|
+
### For True Hot Module Reloading
|
|
80
|
+
1. Implement dynamic `import()` for module reloading
|
|
81
|
+
2. Add module-level cache busting
|
|
82
|
+
3. Handle state preservation during reload
|
|
83
|
+
|
|
84
|
+
### For Enhanced Display
|
|
85
|
+
1. Add streaming responses (real-time message display)
|
|
86
|
+
2. Add more sophisticated metadata visualization
|
|
87
|
+
3. Add export/sharing functionality
|
|
88
|
+
|
|
89
|
+
## 🧪 Testing
|
|
90
|
+
|
|
91
|
+
### Test a Message
|
|
92
|
+
```bash
|
|
93
|
+
CONV=$(curl -s -X POST http://localhost:9897/gm/api/conversations \
|
|
94
|
+
-H "Content-Type: application/json" \
|
|
95
|
+
-d '{"agentId": "claude-code", "title": "Test"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['conversation']['id'])")
|
|
96
|
+
|
|
97
|
+
curl -s -X POST "http://localhost:9897/gm/api/conversations/$CONV/messages" \
|
|
98
|
+
-H "Content-Type: application/json" \
|
|
99
|
+
-d '{"agentId": "claude-code", "content": "Your question here", "idempotencyKey": "test-1"}'
|
|
100
|
+
|
|
101
|
+
# Check response after ~30-50 seconds
|
|
102
|
+
curl -s "http://localhost:9897/gm/api/conversations/$CONV/messages" | python3 -m json.tool
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 📝 Files Structure
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
agentgui/
|
|
109
|
+
├── server.js # Main HTTP server + WebSocket
|
|
110
|
+
├── acp-launcher.js # ACP connection management + system prompt
|
|
111
|
+
├── database.js # SQLite persistence
|
|
112
|
+
├── response-formatter.js # Response parsing & segmentation
|
|
113
|
+
├── html-wrapper.js # Markdown to HTML conversion
|
|
114
|
+
├── hot-reload-manager.js # Hot reload infrastructure (prepared)
|
|
115
|
+
├── static/
|
|
116
|
+
│ ├── app.js # Frontend logic
|
|
117
|
+
│ ├── index.html # UI template
|
|
118
|
+
│ ├── styles.css # Comprehensive styling
|
|
119
|
+
│ └── theme.js # Theme management
|
|
120
|
+
└── package.json # Dependencies
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## 🚀 Running the Server
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Development (port 3000)
|
|
127
|
+
npm start
|
|
128
|
+
|
|
129
|
+
# Production (port 9897)
|
|
130
|
+
PORT=9897 npm start
|
|
131
|
+
|
|
132
|
+
# With hot reload enabled (default)
|
|
133
|
+
PORT=9897 HOT_RELOAD=true npm start
|
|
134
|
+
|
|
135
|
+
# To disable hot reload
|
|
136
|
+
PORT=9897 HOT_RELOAD=false npm start
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 💡 Key Implementation Details
|
|
140
|
+
|
|
141
|
+
### HTML Wrapping Flow
|
|
142
|
+
```
|
|
143
|
+
Claude's plain text response
|
|
144
|
+
↓
|
|
145
|
+
HTMLWrapper.wrapResponse()
|
|
146
|
+
↓
|
|
147
|
+
Parse markdown syntax
|
|
148
|
+
↓
|
|
149
|
+
Convert to HTML with Tailwind classes
|
|
150
|
+
↓
|
|
151
|
+
Wrap in container div
|
|
152
|
+
↓
|
|
153
|
+
Store as messageContent.text
|
|
154
|
+
↓
|
|
155
|
+
Frontend detects HTML (starts with <div)
|
|
156
|
+
↓
|
|
157
|
+
Renders with sanitization
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Response Structure
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"id": "msg-xxx",
|
|
164
|
+
"role": "assistant",
|
|
165
|
+
"content": {
|
|
166
|
+
"text": "<div class=\"space-y-4 p-6\">...HTML...</div>",
|
|
167
|
+
"segments": [...],
|
|
168
|
+
"metadata": {...},
|
|
169
|
+
"updateChunks": [...],
|
|
170
|
+
"blocks": [],
|
|
171
|
+
"isHTML": true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## ✨ Results
|
|
177
|
+
|
|
178
|
+
- All responses now display as beautiful, styled HTML
|
|
179
|
+
- Code blocks are properly syntax-highlighted
|
|
180
|
+
- Metadata (tools, thinking, tasks) are rich and interactive
|
|
181
|
+
- System runs on port 9897 for production
|
|
182
|
+
- OAuth authentication works seamlessly
|
|
183
|
+
- Database persists conversations and history
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
**Last Updated**: February 3, 2026
|
|
188
|
+
**Version**: 1.0.16+
|
|
189
|
+
**Status**: Production Ready (with auto-HTML wrapping)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Response Display Issues & Analysis
|
|
2
|
+
|
|
3
|
+
## Issue 1: Combined Responses Without Separation
|
|
4
|
+
|
|
5
|
+
Example from user feedback:
|
|
6
|
+
```
|
|
7
|
+
"Let me start by reading the PRD file to understand what tasks need to be completed.This is a large PRD with many unchecked items across 6 phases. Let me explore the codebase to understand the current state before planning implementation."
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
**Problem**: Two separate thoughts/steps are combined into one paragraph without proper separation:
|
|
11
|
+
- "Let me start by reading..." (statement of intent)
|
|
12
|
+
- "This is a large PRD..." (observation/analysis)
|
|
13
|
+
|
|
14
|
+
### Root Cause Analysis
|
|
15
|
+
|
|
16
|
+
The ResponseFormatter is parsing continuous text as a single segment if it doesn't have explicit markdown formatting. When Claude sends thinking or analysis steps, they may be:
|
|
17
|
+
1. Separated by newlines in the actual response
|
|
18
|
+
2. Separated by periods/punctuation but no blank lines
|
|
19
|
+
3. Represented as separate agent messages but concatenated
|
|
20
|
+
|
|
21
|
+
### Current Handling
|
|
22
|
+
|
|
23
|
+
In `response-formatter.js`, the `parseResponse()` function treats consecutive text lines as one segment unless they have markdown markers (# ## etc).
|
|
24
|
+
|
|
25
|
+
### Fix Needed
|
|
26
|
+
|
|
27
|
+
1. **Detect step boundaries**: Recognize patterns like:
|
|
28
|
+
- "Let me..." → New action/step
|
|
29
|
+
- "I'll..." → New intent
|
|
30
|
+
- "Now..." → Transition
|
|
31
|
+
- "Here's..." → Result presentation
|
|
32
|
+
- "First..." / "Next..." → Sequential steps
|
|
33
|
+
|
|
34
|
+
2. **Segment by semantic meaning**: Break text into logical paragraphs that represent:
|
|
35
|
+
- Planning/Analysis
|
|
36
|
+
- Investigation
|
|
37
|
+
- Results
|
|
38
|
+
- Explanations
|
|
39
|
+
|
|
40
|
+
3. **Add visual separators**: Use cards or dividers between segments
|
|
41
|
+
|
|
42
|
+
## Issue 2: Tags/JSON Not Rendering
|
|
43
|
+
|
|
44
|
+
Types of content that should render specially:
|
|
45
|
+
- `<thinking>` tags (Claude's reasoning)
|
|
46
|
+
- `<tool_use>` tags (Tool call indicators)
|
|
47
|
+
- `<result>` tags (Tool results)
|
|
48
|
+
- Metadata blocks
|
|
49
|
+
- Tool output
|
|
50
|
+
|
|
51
|
+
Example that should render:
|
|
52
|
+
```
|
|
53
|
+
<thinking>
|
|
54
|
+
This problem requires analysis
|
|
55
|
+
</thinking>
|
|
56
|
+
|
|
57
|
+
<tool_use>
|
|
58
|
+
name: fs_access
|
|
59
|
+
</tool_use>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Issue 3: Metadata-Rich Content
|
|
63
|
+
|
|
64
|
+
Elements that need special rendering:
|
|
65
|
+
- Tool names (should be in code styling)
|
|
66
|
+
- Function signatures (should be formatted as code)
|
|
67
|
+
- API responses (should be formatted as JSON blocks)
|
|
68
|
+
- Task lists (should be checkboxes or special formatting)
|
|
69
|
+
- Subagent notifications (should have special styling)
|
|
70
|
+
|
|
71
|
+
## Solution Architecture
|
|
72
|
+
|
|
73
|
+
### Enhanced ResponseFormatter
|
|
74
|
+
|
|
75
|
+
1. **XML Tag Detection**
|
|
76
|
+
```javascript
|
|
77
|
+
detectXMLTags(text) // Find <thinking>, <tool_use>, <result>, etc.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
2. **Smart Segmentation**
|
|
81
|
+
```javascript
|
|
82
|
+
segmentByIntent(text) // Break on "Let me", "I'll", "Now", etc.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
3. **Special Element Handling**
|
|
86
|
+
```javascript
|
|
87
|
+
renderToolCall(toolData)
|
|
88
|
+
renderThinking(thoughtText)
|
|
89
|
+
renderResult(resultData)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Frontend Enhancement
|
|
93
|
+
|
|
94
|
+
1. **New Segment Types**
|
|
95
|
+
- `thinking` → Collapsible gray box
|
|
96
|
+
- `tool_call` → Highlighted with tool name
|
|
97
|
+
- `tool_result` → Code/result styling
|
|
98
|
+
- `analysis` → Regular text with better spacing
|
|
99
|
+
- `action` → Action statement styling
|
|
100
|
+
|
|
101
|
+
2. **CSS Classes for Each**
|
|
102
|
+
```css
|
|
103
|
+
.segment-thinking { background: #f9f9f9; border-left: 4px solid #999; }
|
|
104
|
+
.segment-tool_call { background: #f0f8ff; border-left: 4px solid #007acc; }
|
|
105
|
+
.segment-tool_result { background: #fff9e6; border-left: 4px solid #ffb300; }
|
|
106
|
+
.segment-action { font-weight: 500; color: #333; margin-top: 1.5rem; }
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Implementation Priority
|
|
110
|
+
|
|
111
|
+
1. **High Priority** (Breaking issues)
|
|
112
|
+
- Fix response combining (split on semantic boundaries)
|
|
113
|
+
- Render `<thinking>` blocks separately
|
|
114
|
+
- Proper code block formatting
|
|
115
|
+
|
|
116
|
+
2. **Medium Priority** (Display quality)
|
|
117
|
+
- Tool call highlighting
|
|
118
|
+
- Tool result formatting
|
|
119
|
+
- Better metadata display
|
|
120
|
+
|
|
121
|
+
3. **Low Priority** (Enhancement)
|
|
122
|
+
- Animated reveals for collapsible sections
|
|
123
|
+
- Copy-to-clipboard for code blocks
|
|
124
|
+
- Export formatting
|
|
125
|
+
|
|
126
|
+
## Files to Modify
|
|
127
|
+
|
|
128
|
+
1. `response-formatter.js`
|
|
129
|
+
- Add XML tag detection
|
|
130
|
+
- Add intent-based segmentation
|
|
131
|
+
- Add special element parsing
|
|
132
|
+
|
|
133
|
+
2. `static/app.js`
|
|
134
|
+
- Add `renderThinkingSegment()`
|
|
135
|
+
- Add `renderToolCallSegment()`
|
|
136
|
+
- Add `renderActionSegment()`
|
|
137
|
+
|
|
138
|
+
3. `static/styles.css`
|
|
139
|
+
- Add styling for new segment types
|
|
140
|
+
- Add visual hierarchy
|
|
141
|
+
|
|
142
|
+
## Testing Strategy
|
|
143
|
+
|
|
144
|
+
Create test cases with responses like:
|
|
145
|
+
```
|
|
146
|
+
Let me analyze this requirement.
|
|
147
|
+
|
|
148
|
+
Looking at the code structure, I see...
|
|
149
|
+
|
|
150
|
+
Now I'll implement the solution.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Should produce:
|
|
154
|
+
- Segment 1: "Let me analyze..." (action/planning)
|
|
155
|
+
- Segment 2: "Looking at..." (analysis)
|
|
156
|
+
- Segment 3: "Now I'll..." (implementation step)
|
|
157
|
+
|
package/html-wrapper.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Wrapper for Claude Responses
|
|
3
|
+
* Converts plain text/markdown responses into beautiful HTML
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class HTMLWrapper {
|
|
7
|
+
/**
|
|
8
|
+
* Wrap plain text response in HTML with RippleUI styling
|
|
9
|
+
*/
|
|
10
|
+
static wrapResponse(text) {
|
|
11
|
+
if (!text || typeof text !== 'string') return text;
|
|
12
|
+
|
|
13
|
+
// If already HTML, return as-is
|
|
14
|
+
if (text.trim().startsWith('<')) return text;
|
|
15
|
+
|
|
16
|
+
// Parse markdown-style text and convert to HTML
|
|
17
|
+
const lines = text.split('\n');
|
|
18
|
+
const html = this.parseMarkdownToHTML(lines);
|
|
19
|
+
|
|
20
|
+
// Wrap in container
|
|
21
|
+
return `<div class="space-y-4 p-6 max-w-4xl">${html}</div>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static parseMarkdownToHTML(lines) {
|
|
25
|
+
let html = '';
|
|
26
|
+
let inCodeBlock = false;
|
|
27
|
+
let codeLanguage = 'text';
|
|
28
|
+
let codeContent = '';
|
|
29
|
+
let listItems = [];
|
|
30
|
+
let inList = false;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
|
|
35
|
+
// Code blocks
|
|
36
|
+
if (line.match(/^```(\w+)?$/)) {
|
|
37
|
+
if (!inCodeBlock) {
|
|
38
|
+
// Start code block
|
|
39
|
+
inCodeBlock = true;
|
|
40
|
+
codeLanguage = line.match(/```(\w+)?/)?.[1] || 'text';
|
|
41
|
+
codeContent = '';
|
|
42
|
+
} else {
|
|
43
|
+
// End code block
|
|
44
|
+
inCodeBlock = false;
|
|
45
|
+
html += `<pre class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto"><code class="language-${codeLanguage}">${this.escapeHtml(codeContent)}</code></pre>`;
|
|
46
|
+
codeContent = '';
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (inCodeBlock) {
|
|
52
|
+
codeContent += (codeContent ? '\n' : '') + line;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Headings
|
|
57
|
+
if (line.match(/^#+\s/)) {
|
|
58
|
+
const level = line.match(/^#+/)[0].length;
|
|
59
|
+
const heading = line.replace(/^#+\s/, '');
|
|
60
|
+
html += `<h${level} class="text-${level === 1 ? '3xl' : level === 2 ? '2xl' : 'xl'} font-bold text-gray-900 mt-4">${this.escapeHtml(heading)}</h${level}>`;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Lists
|
|
65
|
+
if (line.match(/^[-*•]\s/) || line.match(/^\d+\.\s/)) {
|
|
66
|
+
const itemText = line.replace(/^[-*•\d+.]\s+/, '');
|
|
67
|
+
if (!inList) {
|
|
68
|
+
inList = true;
|
|
69
|
+
listItems = [];
|
|
70
|
+
}
|
|
71
|
+
listItems.push(itemText);
|
|
72
|
+
continue;
|
|
73
|
+
} else if (inList && line.trim()) {
|
|
74
|
+
// End list
|
|
75
|
+
html += `<ul class="list-none space-y-2 ml-0"><li class="p-3 bg-gray-100 rounded border-l-4 border-blue-500">${listItems.map(item => `• ${this.escapeHtml(item)}`).join('</li><li class="p-3 bg-gray-100 rounded border-l-4 border-blue-500">')}</li></ul>`;
|
|
76
|
+
inList = false;
|
|
77
|
+
listItems = [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Empty lines
|
|
81
|
+
if (!line.trim()) {
|
|
82
|
+
if (!html.endsWith('</p>')) html += '<br>';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Regular paragraphs
|
|
87
|
+
if (line.trim()) {
|
|
88
|
+
// Format inline markdown
|
|
89
|
+
let formatted = this.escapeHtml(line);
|
|
90
|
+
formatted = formatted.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold">$1</strong>');
|
|
91
|
+
formatted = formatted.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>');
|
|
92
|
+
formatted = formatted.replace(/`([^`]+)`/g, '<code class="bg-gray-200 px-2 py-1 rounded font-mono text-sm">$1</code>');
|
|
93
|
+
|
|
94
|
+
html += `<p class="text-gray-700 leading-relaxed">${formatted}</p>`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Close any remaining list
|
|
99
|
+
if (inList) {
|
|
100
|
+
html += `<ul class="list-none space-y-2 ml-0"><li class="p-3 bg-gray-100 rounded border-l-4 border-blue-500">${listItems.map(item => `• ${this.escapeHtml(item)}`).join('</li><li class="p-3 bg-gray-100 rounded border-l-4 border-blue-500">')}</li></ul>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return html;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static escapeHtml(text) {
|
|
107
|
+
if (typeof text !== 'string') return '';
|
|
108
|
+
return text
|
|
109
|
+
.replace(/&/g, '&')
|
|
110
|
+
.replace(/</g, '<')
|
|
111
|
+
.replace(/>/g, '>')
|
|
112
|
+
.replace(/"/g, '"')
|
|
113
|
+
.replace(/'/g, ''');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default HTMLWrapper;
|
package/package.json
CHANGED
package/response-formatter.js
CHANGED
|
@@ -138,49 +138,17 @@ export class ResponseFormatter {
|
|
|
138
138
|
static segmentResponse(text) {
|
|
139
139
|
if (!text || typeof text !== 'string') return [];
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
141
|
+
// First, extract XML tags
|
|
142
|
+
const xmlSegments = this.extractXMLTags(text);
|
|
143
|
+
if (xmlSegments.length > 0) {
|
|
144
|
+
return xmlSegments;
|
|
177
145
|
}
|
|
178
146
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
147
|
+
// Otherwise, segment by intent and transitions
|
|
148
|
+
const segments = [];
|
|
149
|
+
const parts = this.segmentByIntent(text);
|
|
182
150
|
|
|
183
|
-
//
|
|
151
|
+
// Parse each part
|
|
184
152
|
for (const part of parts) {
|
|
185
153
|
segments.push({
|
|
186
154
|
...part,
|
|
@@ -192,6 +160,82 @@ export class ResponseFormatter {
|
|
|
192
160
|
return segments;
|
|
193
161
|
}
|
|
194
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Extract XML-tagged content as separate segments
|
|
165
|
+
*/
|
|
166
|
+
static extractXMLTags(text) {
|
|
167
|
+
const xmlPattern = /<(thinking|tool_use|tool_result|result|action)[\s>]([\s\S]*?)<\/\1>/gi;
|
|
168
|
+
const segments = [];
|
|
169
|
+
let lastIndex = 0;
|
|
170
|
+
let match;
|
|
171
|
+
|
|
172
|
+
while ((match = xmlPattern.exec(text)) !== null) {
|
|
173
|
+
const before = text.substring(lastIndex, match.index);
|
|
174
|
+
if (before.trim()) {
|
|
175
|
+
segments.push({ type: 'text', text: before.trim() });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const tagType = match[1].toLowerCase();
|
|
179
|
+
const tagContent = match[2].trim();
|
|
180
|
+
segments.push({ type: tagType, text: tagContent });
|
|
181
|
+
lastIndex = xmlPattern.lastIndex;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (lastIndex < text.length) {
|
|
185
|
+
const remaining = text.substring(lastIndex).trim();
|
|
186
|
+
if (remaining) {
|
|
187
|
+
segments.push({ type: 'text', text: remaining });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return segments.length > 0 ? segments : [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Segment by semantic intent/actions
|
|
196
|
+
*/
|
|
197
|
+
static segmentByIntent(text) {
|
|
198
|
+
const segments = [];
|
|
199
|
+
const intentPatterns = [
|
|
200
|
+
{ pattern: /^(Let me|I'll|I'm going to|First,?|Next,?|Now,?|Here's)/im, type: 'action' },
|
|
201
|
+
{ pattern: /^(Looking|Examining|Analyzing|Reviewing|Checking|Reading)/im, type: 'analysis' },
|
|
202
|
+
{ pattern: /^(Here'?s|Result|Output|Found|Got|Completed)/im, type: 'result' },
|
|
203
|
+
{ pattern: /^(The|This|That|These|Those)/im, type: 'explanation' }
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
let currentSegment = '';
|
|
207
|
+
let currentType = 'text';
|
|
208
|
+
const lines = text.split('\n');
|
|
209
|
+
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
let newType = currentType;
|
|
212
|
+
|
|
213
|
+
// Check for intent pattern match
|
|
214
|
+
for (const { pattern, type } of intentPatterns) {
|
|
215
|
+
if (pattern.test(line)) {
|
|
216
|
+
newType = type;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// If type changed and we have content, save segment
|
|
222
|
+
if (newType !== currentType && currentSegment.trim()) {
|
|
223
|
+
segments.push({ type: currentType, text: currentSegment.trim() });
|
|
224
|
+
currentSegment = '';
|
|
225
|
+
currentType = newType;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
currentSegment += (currentSegment ? '\n' : '') + line;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Add remaining segment
|
|
232
|
+
if (currentSegment.trim()) {
|
|
233
|
+
segments.push({ type: currentType, text: currentSegment.trim() });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return segments.length > 0 ? segments : [{ type: 'text', text }];
|
|
237
|
+
}
|
|
238
|
+
|
|
195
239
|
/**
|
|
196
240
|
* Format segments for display with proper HTML
|
|
197
241
|
*/
|
package/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { execSync } from 'child_process';
|
|
|
8
8
|
import { queries } from './database.js';
|
|
9
9
|
import ACPConnection from './acp-launcher.js';
|
|
10
10
|
import { ResponseFormatter } from './response-formatter.js';
|
|
11
|
+
import { HTMLWrapper } from './html-wrapper.js';
|
|
11
12
|
|
|
12
13
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const PORT = process.env.PORT || 3000;
|
|
@@ -337,7 +338,13 @@ async function processMessage(conversationId, messageId, sessionId, content, age
|
|
|
337
338
|
const result = await conn.sendPrompt(content);
|
|
338
339
|
conn.onUpdate = null;
|
|
339
340
|
|
|
340
|
-
|
|
341
|
+
let responseText = fullText || result?.result || (result?.stopReason ? `Completed: ${result.stopReason}` : 'No response.');
|
|
342
|
+
|
|
343
|
+
// Wrap response in HTML if it's not already
|
|
344
|
+
const isHTML = responseText.trim().startsWith('<');
|
|
345
|
+
if (!isHTML) {
|
|
346
|
+
responseText = HTMLWrapper.wrapResponse(responseText);
|
|
347
|
+
}
|
|
341
348
|
|
|
342
349
|
// Segment and format the response for better display
|
|
343
350
|
const segments = ResponseFormatter.segmentResponse(responseText);
|
|
@@ -348,12 +355,14 @@ async function processMessage(conversationId, messageId, sessionId, content, age
|
|
|
348
355
|
blocks,
|
|
349
356
|
segments,
|
|
350
357
|
metadata,
|
|
351
|
-
updateChunks
|
|
358
|
+
updateChunks,
|
|
359
|
+
isHTML: true
|
|
352
360
|
} : {
|
|
353
361
|
text: responseText,
|
|
354
362
|
segments,
|
|
355
363
|
metadata,
|
|
356
|
-
updateChunks
|
|
364
|
+
updateChunks,
|
|
365
|
+
isHTML: true
|
|
357
366
|
};
|
|
358
367
|
|
|
359
368
|
const assistantMessage = queries.createMessage(conversationId, 'assistant', messageContent);
|
package/static/app.js
CHANGED
|
@@ -677,6 +677,54 @@ class GMGUIApp {
|
|
|
677
677
|
li.className = 'response-list-item';
|
|
678
678
|
li.textContent = segment.content;
|
|
679
679
|
el.appendChild(li);
|
|
680
|
+
} else if (segment.type === 'thinking') {
|
|
681
|
+
// Collapsible thinking block
|
|
682
|
+
const details = document.createElement('details');
|
|
683
|
+
details.className = 'segment-thinking';
|
|
684
|
+
const summary = document.createElement('summary');
|
|
685
|
+
summary.textContent = '💭 Thinking';
|
|
686
|
+
details.appendChild(summary);
|
|
687
|
+
const content = document.createElement('div');
|
|
688
|
+
content.className = 'thinking-content';
|
|
689
|
+
content.textContent = segment.text;
|
|
690
|
+
details.appendChild(content);
|
|
691
|
+
el.appendChild(details);
|
|
692
|
+
} else if (segment.type === 'tool_use') {
|
|
693
|
+
// Tool call highlight
|
|
694
|
+
const div = document.createElement('div');
|
|
695
|
+
div.className = 'segment-tool-use';
|
|
696
|
+
div.innerHTML = `<div class="tool-icon">⚙️ Tool Call</div><pre class="tool-content"><code>${this.escapeHtml(segment.text)}</code></pre>`;
|
|
697
|
+
el.appendChild(div);
|
|
698
|
+
} else if (segment.type === 'tool_result') {
|
|
699
|
+
// Tool result
|
|
700
|
+
const div = document.createElement('div');
|
|
701
|
+
div.className = 'segment-tool-result';
|
|
702
|
+
div.innerHTML = `<div class="result-icon">📦 Result</div><pre class="result-content"><code>${this.escapeHtml(segment.text)}</code></pre>`;
|
|
703
|
+
el.appendChild(div);
|
|
704
|
+
} else if (segment.type === 'action') {
|
|
705
|
+
// Action statement - bold and prominent
|
|
706
|
+
const p = document.createElement('p');
|
|
707
|
+
p.className = 'response-action';
|
|
708
|
+
p.innerHTML = `<strong>→ ${this.escapeHtml(segment.text)}</strong>`;
|
|
709
|
+
el.appendChild(p);
|
|
710
|
+
} else if (segment.type === 'analysis') {
|
|
711
|
+
// Analysis/investigation
|
|
712
|
+
const p = document.createElement('p');
|
|
713
|
+
p.className = 'response-analysis';
|
|
714
|
+
p.innerHTML = `<em>🔍 ${this.escapeHtml(segment.text)}</em>`;
|
|
715
|
+
el.appendChild(p);
|
|
716
|
+
} else if (segment.type === 'result') {
|
|
717
|
+
// Result presentation
|
|
718
|
+
const div = document.createElement('div');
|
|
719
|
+
div.className = 'response-result';
|
|
720
|
+
div.innerHTML = segment.text
|
|
721
|
+
.replace(/&/g, '&')
|
|
722
|
+
.replace(/</g, '<')
|
|
723
|
+
.replace(/>/g, '>')
|
|
724
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
725
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
726
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
727
|
+
el.appendChild(div);
|
|
680
728
|
} else if (segment.type === 'text') {
|
|
681
729
|
const p = document.createElement('p');
|
|
682
730
|
p.className = 'response-text';
|
|
@@ -693,6 +741,17 @@ class GMGUIApp {
|
|
|
693
741
|
return el;
|
|
694
742
|
}
|
|
695
743
|
|
|
744
|
+
escapeHtml(text) {
|
|
745
|
+
if (typeof text !== 'string') return '';
|
|
746
|
+
return text
|
|
747
|
+
.replace(/&/g, '&')
|
|
748
|
+
.replace(/</g, '<')
|
|
749
|
+
.replace(/>/g, '>')
|
|
750
|
+
.replace(/"/g, '"')
|
|
751
|
+
.replace(/'/g, ''');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
696
755
|
renderMetadata(metadata) {
|
|
697
756
|
if (!metadata || Object.keys(metadata).every(k => !metadata[k] || metadata[k].length === 0)) {
|
|
698
757
|
return null;
|
package/static/styles.css
CHANGED
|
@@ -1578,3 +1578,115 @@ p code {
|
|
|
1578
1578
|
content: '✓ ';
|
|
1579
1579
|
color: #28a745;
|
|
1580
1580
|
}
|
|
1581
|
+
|
|
1582
|
+
/* New Segment Types */
|
|
1583
|
+
|
|
1584
|
+
/* Thinking blocks */
|
|
1585
|
+
.segment-thinking {
|
|
1586
|
+
background: #f9f9f9;
|
|
1587
|
+
border-left: 4px solid #999;
|
|
1588
|
+
padding: 1rem;
|
|
1589
|
+
margin: 1rem 0;
|
|
1590
|
+
border-radius: 4px;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.segment-thinking summary {
|
|
1594
|
+
cursor: pointer;
|
|
1595
|
+
font-weight: 600;
|
|
1596
|
+
color: #666;
|
|
1597
|
+
user-select: none;
|
|
1598
|
+
padding-bottom: 0.5rem;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
.segment-thinking summary:hover {
|
|
1602
|
+
color: #333;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
.thinking-content {
|
|
1606
|
+
color: #555;
|
|
1607
|
+
font-size: 0.95em;
|
|
1608
|
+
line-height: 1.6;
|
|
1609
|
+
margin-top: 0.5rem;
|
|
1610
|
+
padding: 0.5rem 0;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/* Tool use blocks */
|
|
1614
|
+
.segment-tool-use {
|
|
1615
|
+
background: #f0f8ff;
|
|
1616
|
+
border-left: 4px solid #007acc;
|
|
1617
|
+
padding: 1rem;
|
|
1618
|
+
margin: 1rem 0;
|
|
1619
|
+
border-radius: 4px;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
.tool-icon {
|
|
1623
|
+
font-weight: 600;
|
|
1624
|
+
color: #007acc;
|
|
1625
|
+
margin-bottom: 0.5rem;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.tool-content {
|
|
1629
|
+
background: #f5f5f5;
|
|
1630
|
+
padding: 0.75rem;
|
|
1631
|
+
border-radius: 3px;
|
|
1632
|
+
overflow-x: auto;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.tool-content code {
|
|
1636
|
+
color: #d73a49;
|
|
1637
|
+
font-family: 'Courier New', monospace;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/* Tool result blocks */
|
|
1641
|
+
.segment-tool-result {
|
|
1642
|
+
background: #fff9e6;
|
|
1643
|
+
border-left: 4px solid #ffb300;
|
|
1644
|
+
padding: 1rem;
|
|
1645
|
+
margin: 1rem 0;
|
|
1646
|
+
border-radius: 4px;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.result-icon {
|
|
1650
|
+
font-weight: 600;
|
|
1651
|
+
color: #997700;
|
|
1652
|
+
margin-bottom: 0.5rem;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
.result-content {
|
|
1656
|
+
background: #f5f5f5;
|
|
1657
|
+
padding: 0.75rem;
|
|
1658
|
+
border-radius: 3px;
|
|
1659
|
+
overflow-x: auto;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/* Action statements */
|
|
1663
|
+
.response-action {
|
|
1664
|
+
background: #e8f5e9;
|
|
1665
|
+
border-left: 4px solid #28a745;
|
|
1666
|
+
padding: 0.75rem 1rem;
|
|
1667
|
+
margin: 1.5rem 0 0.5rem 0;
|
|
1668
|
+
border-radius: 4px;
|
|
1669
|
+
font-weight: 500;
|
|
1670
|
+
color: #1b5e20;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/* Analysis blocks */
|
|
1674
|
+
.response-analysis {
|
|
1675
|
+
background: #e3f2fd;
|
|
1676
|
+
border-left: 4px solid #1976d2;
|
|
1677
|
+
padding: 0.75rem 1rem;
|
|
1678
|
+
margin: 1rem 0;
|
|
1679
|
+
border-radius: 4px;
|
|
1680
|
+
color: #0d47a1;
|
|
1681
|
+
font-size: 0.95em;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/* Result presentation */
|
|
1685
|
+
.response-result {
|
|
1686
|
+
background: #f3e5f5;
|
|
1687
|
+
border-left: 4px solid #7b1fa2;
|
|
1688
|
+
padding: 1rem;
|
|
1689
|
+
margin: 1rem 0;
|
|
1690
|
+
border-radius: 4px;
|
|
1691
|
+
line-height: 1.6;
|
|
1692
|
+
}
|