agentgui 1.0.15 → 1.0.17

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.
@@ -0,0 +1,209 @@
1
+ # AgentGUI Recent Updates
2
+
3
+ ## Overview
4
+ AgentGUI has been significantly enhanced to use local Claude Code OAuth authentication and provide rich, metadata-aware response rendering.
5
+
6
+ ## Key Changes
7
+
8
+ ### 1. OAuth Authentication via Local Claude Code
9
+ **File**: `acp-launcher.js`, `server.js`
10
+
11
+ - **Binary Discovery**: Automatically finds `claude-code-acp` in common installation locations
12
+ - `/config/.gmweb/npm-global/bin/claude-code-acp`
13
+ - `~/.local/bin/claude-code-acp`
14
+ - Falls back to PATH if configured
15
+
16
+ - **PATH Management**: Enhanced environment variable handling to ensure npm global binaries are discoverable
17
+
18
+ - **Timeout Optimization**:
19
+ - Initialize: 4s → 10s
20
+ - Session creation: 4s → 30s
21
+ - Mode setting: 2s → 10s
22
+ - Handshake deadline: 5s → 60s
23
+
24
+ **Benefits**:
25
+ - Uses existing Claude Code authentication
26
+ - No additional API key setup needed
27
+ - Seamless integration with system auth
28
+
29
+ ### 2. Response Segmentation & Metadata Extraction
30
+ **Files**: `response-formatter.js`, `server.js`, `static/app.js`
31
+
32
+ #### ResponseFormatter
33
+ - Intelligent parsing of Claude responses into semantic units:
34
+ - Code blocks (with language detection)
35
+ - Headings (h1-h6)
36
+ - Text paragraphs
37
+ - Blockquotes
38
+ - Lists
39
+
40
+ - Metadata extraction:
41
+ - Tool calls and function names
42
+ - Thinking/reasoning blocks
43
+ - Task references
44
+ - Subagent usage
45
+
46
+ - Proper formatting with inline code highlighting
47
+
48
+ #### Frontend Rendering
49
+ - `renderSegment()`: Beautiful display of each segment type
50
+ - `renderMetadata()`: Rich metadata sidebar with:
51
+ - Tools used (with code highlighting)
52
+ - Thinking blocks (collapsible details element)
53
+ - Subagents employed
54
+ - Tasks referenced
55
+
56
+ ### 3. Rich HTML/RippleUI Responses
57
+ **Files**: `acp-launcher.js`, `static/app.js`
58
+
59
+ #### System Prompt Enhancement
60
+ Comprehensive instruction set forcing Claude to respond with HTML:
61
+ - RippleUI components (cards, alerts, tables, badges)
62
+ - Tailwind CSS styling
63
+ - Semantic HTML structure
64
+ - Code block formatting with language hints
65
+ - Never raw text - always wrapped HTML
66
+
67
+ #### HTML Detection Improvement
68
+ - Detects HTML by tags, structure, AND Tailwind classes
69
+ - Lower tag count threshold for detection (2 vs 3)
70
+ - Better recognition of styled components
71
+
72
+ ### 4. Professional CSS Styling
73
+ **File**: `static/styles.css`
74
+
75
+ Added comprehensive styling for:
76
+ - Code blocks with language-specific colors
77
+ - Inline code with syntax highlighting
78
+ - Markdown formatting (bold, italic, code)
79
+ - Collapsible thinking blocks
80
+ - Metadata sections with visual hierarchy
81
+ - Print-friendly styles
82
+ - Dark mode support
83
+
84
+ ### 5. Hot Reload Infrastructure
85
+ **File**: `hot-reload-manager.js`
86
+
87
+ Prepared for hot reloading:
88
+ - File watching with debouncing
89
+ - WebSocket-based reload signaling
90
+ - Graceful client-side reloading
91
+ - Ready for future implementation
92
+
93
+ ## System Architecture
94
+
95
+ ### Ports
96
+ - Production: **9897** (via system startup)
97
+ - Development: **3000** (via `npm start`)
98
+
99
+ ### Request Flow
100
+ ```
101
+ User Message
102
+
103
+ API endpoint (/api/conversations/:id/messages)
104
+
105
+ Server (processMessage)
106
+
107
+ getACP() - Gets or creates connection
108
+
109
+ conn.sendPrompt() - Sends to Claude Code via ACP bridge
110
+
111
+ ResponseFormatter.segmentResponse()
112
+
113
+ Database storage with segments + metadata
114
+
115
+ Frontend display with rich rendering
116
+ ```
117
+
118
+ ### Connection Management
119
+ - ACP Pool: Maintains persistent connections per agent
120
+ - OAuth via local Claude Code credentials
121
+ - Graceful error handling and fallback
122
+ - Automatic reconnection
123
+
124
+ ## Display Examples
125
+
126
+ ### Segmented Response
127
+ Plain text becomes:
128
+ ```
129
+ [Heading] Problem Analysis
130
+ [Text] Explanation paragraph
131
+ [Code] javascript function example
132
+ [Metadata] Tools used, reasoning blocks
133
+ ```
134
+
135
+ ### Metadata Display
136
+ - **Tools Used**: List of functions/tools called
137
+ - **Reasoning**: Collapsible thinking process
138
+ - **Subagents**: External agents employed
139
+ - **Tasks**: Itemized task list
140
+
141
+ ## Configuration
142
+
143
+ ### Environment Variables
144
+ ```bash
145
+ PORT=9897 # Server port (default: 3000)
146
+ BASE_URL=/gm # Route prefix (default: /gm)
147
+ HOT_RELOAD=false # Disable hot reload (default: true)
148
+ ```
149
+
150
+ ### Database
151
+ - Location: `~/.gmgui/data.db`
152
+ - Persists conversations, messages, sessions
153
+ - Auto-created on first run
154
+
155
+ ## Testing
156
+
157
+ ### Quick Start
158
+ ```bash
159
+ npm start # Start on port 3000
160
+ PORT=9897 npm start # Start on port 9897
161
+ ```
162
+
163
+ ### Test Endpoints
164
+ ```bash
165
+ # Get agents
166
+ curl http://localhost:3000/gm/api/agents
167
+
168
+ # Create conversation
169
+ curl -X POST http://localhost:3000/gm/api/conversations \
170
+ -H "Content-Type: application/json" \
171
+ -d '{"agentId": "claude-code", "title": "Test"}'
172
+
173
+ # Send message
174
+ curl -X POST http://localhost:3000/gm/api/conversations/{id}/messages \
175
+ -H "Content-Type: application/json" \
176
+ -d '{"agentId": "claude-code", "content": "Hello"}'
177
+
178
+ # Get messages
179
+ curl http://localhost:3000/gm/api/conversations/{id}/messages
180
+ ```
181
+
182
+ ## Future Enhancements
183
+
184
+ 1. **Full Hot Reloading**: Complete implementation of HotReloadManager
185
+ 2. **Streaming Responses**: Real-time message streaming to client
186
+ 3. **Task Tracking**: Enhanced task and subagent visualization
187
+ 4. **Export Functions**: Share/export conversations as HTML/PDF
188
+ 5. **Theme Customization**: Allow user-defined themes
189
+
190
+ ## Files Modified
191
+
192
+ - `acp-launcher.js` - OAuth and timeout fixes, system prompt
193
+ - `server.js` - Response segmentation, metadata extraction
194
+ - `static/app.js` - Rich rendering, HTML detection
195
+ - `static/styles.css` - Professional styling for all components
196
+ - `response-formatter.js` - NEW: Response parsing and formatting
197
+ - `hot-reload-manager.js` - NEW: Hot reload infrastructure
198
+
199
+ ## Git Commits
200
+
201
+ 1. **Fix OAuth Connection** - Binary discovery and PATH management
202
+ 2. **Increase ACP Timeouts** - Proper timeout values for connection establishment
203
+ 3. **Rich Response Formatting** - Segmentation and metadata rendering
204
+ 4. **Enforce HTML Responses** - System prompt and detection improvements
205
+
206
+ ---
207
+
208
+ **Last Updated**: February 3, 2026
209
+ **Version**: 1.0.15
package/acp-launcher.js CHANGED
@@ -33,19 +33,80 @@ 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 INSTRUCTION: You are responding in a web-based HTML interface. EVERY response must be formatted as beautiful, styled HTML using RippleUI and Tailwind CSS. This is NOT a text-based interface - users see raw HTML rendered in their browser.
37
+
38
+ YOUR RESPONSE FORMAT MUST BE:
39
+ Wrap your ENTIRE response in a single HTML container with these elements:
40
+
41
+ \`\`\`html
42
+ <div class="space-y-4 p-6 max-w-4xl">
43
+ <!-- Main content goes here -->
44
+ </div>
45
+ \`\`\`
46
+
47
+ STRUCTURE YOUR RESPONSES LIKE THIS:
48
+
49
+ For questions/answers:
50
+ \`\`\`html
51
+ <div class="space-y-4 p-6">
52
+ <h2 class="text-2xl font-bold text-gray-900">Your Answer</h2>
53
+ <div class="card bg-blue-50 border-l-4 border-blue-500 p-4">
54
+ <p class="text-gray-700">Your detailed answer here</p>
55
+ </div>
56
+ </div>
57
+ \`\`\`
58
+
59
+ For code:
60
+ \`\`\`html
61
+ <div class="space-y-4 p-6">
62
+ <h3 class="text-xl font-bold">Code Example</h3>
63
+ <pre class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto"><code>// Your code here
64
+ function example() { }</code></pre>
65
+ </div>
66
+ \`\`\`
67
+
68
+ For lists:
69
+ \`\`\`html
70
+ <div class="space-y-4 p-6">
71
+ <h3 class="text-xl font-bold">Items</h3>
72
+ <ul class="list-none space-y-2">
73
+ <li class="p-3 bg-gray-100 rounded border-l-4 border-gray-400">• Item one</li>
74
+ <li class="p-3 bg-gray-100 rounded border-l-4 border-gray-400">• Item two</li>
75
+ </ul>
76
+ </div>
77
+ \`\`\`
78
+
79
+ COMPONENT LIBRARY:
80
+ - Card: <div class="card bg-white shadow-lg p-6 rounded-lg"><h4 class="font-bold">Title</h4><p>Content</p></div>
81
+ - Alert: <div class="alert bg-red-100 border-l-4 border-red-500 p-4"><span class="text-red-800">Warning message</span></div>
82
+ - Success: <div class="alert bg-green-100 border-l-4 border-green-500 p-4"><span class="text-green-800">Success</span></div>
83
+ - Table: <table class="w-full border-collapse border border-gray-300"><thead class="bg-gray-100"><tr><th class="p-2 text-left">Col</th></tr></thead><tbody><tr><td class="p-2 border border-gray-300">Data</td></tr></tbody></table>
84
+ - Badge: <span class="inline-block bg-blue-500 text-white px-3 py-1 rounded-full text-sm">Label</span>
85
+ - Code inline: <code class="bg-gray-200 px-2 py-1 rounded text-red-600 font-mono">code</code>
86
+
87
+ MANDATORY RULES:
88
+ ✓ EVERY response MUST be wrapped in a div with class "space-y-4 p-6"
89
+ ✓ Use semantic HTML: <h1>-<h6>, <p>, <ul>, <ol>, <table>, <pre>
90
+ ✓ Always add Tailwind classes for styling: colors, padding, margins, rounded corners
91
+ ✓ Code blocks MUST use <pre><code> with language class like \`class="language-javascript"\`
92
+ ✓ NEVER send plain text without HTML wrapping
93
+ ✓ NEVER respond outside of HTML container
94
+ ✓ Use color classes: text-gray-700, bg-blue-50, border-blue-500
95
+ ✓ Make visual hierarchy clear: use different font sizes, colors, cards
96
+
97
+ EXAMPLES OF COMPLETE RESPONSES:
98
+
99
+ Example 1 - Answer:
100
+ <div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Explanation</h2><p class="text-gray-700">Here is the detailed explanation...</p></div>
101
+
102
+ Example 2 - Code:
103
+ <div class="space-y-4 p-6"><h3 class="text-xl font-bold">JavaScript Function</h3><pre class="bg-gray-900 text-white p-4 rounded overflow-x-auto"><code>const greet = () => console.log('Hello');</code></pre></div>
104
+
105
+ Example 3 - Multiple sections:
106
+ <div class="space-y-4 p-6"><h2 class="text-2xl font-bold">Topic</h2><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 1</h3><p>Content here</p></div><div class="card bg-white shadow p-4"><h3 class="font-bold">Section 2</h3><p>More content</p></div></div>
107
+
108
+ YOU MUST ALWAYS OUTPUT VALID, COMPLETE HTML.
109
+ The user's interface shows YOUR HTML directly - make it beautiful, well-organized, and professional.`;
49
110
 
50
111
  export default class ACPConnection {
51
112
  constructor() {
@@ -255,13 +316,44 @@ export default class ACPConnection {
255
316
  return this.sendRequest('session/set_mode', { sessionId: this.sessionId, modeId });
256
317
  }
257
318
 
258
- async injectSkills() {
319
+ async injectSkills(additionalContext = '') {
259
320
  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);
321
+
322
+ // Combine the system prompt with any additional context
323
+ const systemPrompt = additionalContext ? `${RIPPLEUI_SYSTEM_PROMPT}\n\n---\n\n${additionalContext}` : RIPPLEUI_SYSTEM_PROMPT;
324
+
325
+ try {
326
+ const result = await this.sendRequest('session/skill_inject', {
327
+ sessionId: this.sessionId,
328
+ skills: [],
329
+ notification: [{ type: 'text', text: systemPrompt }]
330
+ });
331
+ return result;
332
+ } catch (e) {
333
+ // skill_inject may not be supported, try alternative methods
334
+ console.log(`[ACP] skill_inject failed (may not be supported): ${e.message}`);
335
+ return null;
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Inject system prompt as initial context
341
+ */
342
+ async injectSystemContext() {
343
+ if (this.printMode) return {};
344
+
345
+ // Some versions may need the system prompt sent as the first message context
346
+ try {
347
+ await this.sendRequest('session/context', {
348
+ sessionId: this.sessionId,
349
+ context: RIPPLEUI_SYSTEM_PROMPT,
350
+ role: 'system'
351
+ });
352
+ return { success: true };
353
+ } catch (e) {
354
+ // This method may not be supported, silently fail
355
+ return null;
356
+ }
265
357
  }
266
358
 
267
359
  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.15",
3
+ "version": "1.0.17",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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
- 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();
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
- 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