agentgui 1.0.396 → 1.0.398

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.396",
3
+ "version": "1.0.398",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -17,7 +17,8 @@
17
17
  "homepage": "https://github.com/AnEntrypoint/agentgui#readme",
18
18
  "scripts": {
19
19
  "start": "node server.js",
20
- "dev": "node server.js --watch"
20
+ "dev": "node server.js --watch",
21
+ "postinstall": "node scripts/patch-fsbrowse.js"
21
22
  },
22
23
  "dependencies": {
23
24
  "@anthropic-ai/claude-code": "^2.1.37",
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Patch script to fix Windows path duplication issue in fsbrowse
4
+ * Fixes: Error ENOENT: no such file or directory, scandir 'C:\C:\dev'
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ const fsbrowsePath = path.join(__dirname, '..', 'node_modules', 'fsbrowse', 'index.js');
14
+
15
+ if (!fs.existsSync(fsbrowsePath)) {
16
+ console.warn('[PATCH] fsbrowse not found, skipping patch');
17
+ process.exit(0);
18
+ }
19
+
20
+ try {
21
+ let content = fs.readFileSync(fsbrowsePath, 'utf8');
22
+
23
+ // Check if patch is already applied
24
+ if (content.includes('sanitizedIsAbsoluteOnDrive')) {
25
+ console.log('[PATCH] fsbrowse Windows path fix already applied');
26
+ process.exit(0);
27
+ }
28
+
29
+ // Replace the makeResolver function with the fixed version
30
+ const oldMakeResolver = `function makeResolver(baseDir) {
31
+ return function resolveWithBaseDir(relPath) {
32
+ const sanitized = sanitizePath(relPath);
33
+ const fullPath = path.resolve(baseDir, sanitized);
34
+ if (!fullPath.startsWith(baseDir)) {
35
+ return { ok: false, error: 'EPATHINJECTION' };
36
+ }
37
+ return { ok: true, path: fullPath };
38
+ };
39
+ }`;
40
+
41
+ const newMakeResolver = `function makeResolver(baseDir) {
42
+ const normalizedBase = path.normalize(baseDir);
43
+ const baseDriveLetter = normalizedBase.match(/^[A-Z]:/i)?.[0];
44
+
45
+ return function resolveWithBaseDir(relPath) {
46
+ const sanitized = sanitizePath(relPath);
47
+ let fullPath;
48
+
49
+ // Extract drive letter from both paths to check for same-drive duplication on Windows
50
+ const sanitizedDriveLetter = sanitized.match(/^[A-Z]:/i)?.[0];
51
+ const sanitizedIsAbsoluteOnDrive = /^[A-Z]:/i.test(sanitized);
52
+
53
+ // If both paths are on the same Windows drive, strip the drive letter from relPath
54
+ // to avoid duplication like C:\\C:\\dev
55
+ if (baseDriveLetter && sanitizedIsAbsoluteOnDrive && sanitizedDriveLetter === baseDriveLetter) {
56
+ // Remove drive letter and leading slashes to make it relative
57
+ const relativePath = sanitized.replace(/^[A-Z]:[\/\\]?/i, '');
58
+ fullPath = path.resolve(normalizedBase, relativePath);
59
+ } else {
60
+ fullPath = path.resolve(normalizedBase, sanitized);
61
+ }
62
+
63
+ // Normalize for consistent comparison
64
+ const normalizedFullPath = path.normalize(fullPath);
65
+ const normalizedComparisonBase = path.normalize(normalizedBase);
66
+
67
+ // Check path injection - convert backslashes to forward slashes for comparison
68
+ const normalizedCheck = normalizedFullPath.replace(/\\\\/g, '/');
69
+ const normalizedBaseCheck = normalizedComparisonBase.replace(/\\\\/g, '/');
70
+
71
+ if (!normalizedCheck.startsWith(normalizedBaseCheck)) {
72
+ return { ok: false, error: 'EPATHINJECTION' };
73
+ }
74
+ return { ok: true, path: normalizedFullPath };
75
+ };
76
+ }`;
77
+
78
+ if (content.includes(oldMakeResolver)) {
79
+ content = content.replace(oldMakeResolver, newMakeResolver);
80
+ fs.writeFileSync(fsbrowsePath, content, 'utf8');
81
+ console.log('[PATCH] fsbrowse Windows path fix applied successfully');
82
+ } else {
83
+ console.warn('[PATCH] Could not find makeResolver function to patch');
84
+ }
85
+ } catch (err) {
86
+ console.error('[PATCH] Error applying fsbrowse patch:', err.message);
87
+ process.exit(1);
88
+ }
package/server.js CHANGED
@@ -295,8 +295,10 @@ expressApp.use(BASE_URL + '/files/:conversationId', (req, res, next) => {
295
295
  if (!conv || !conv.workingDirectory) {
296
296
  return res.status(404).json({ error: 'Conversation not found or no working directory' });
297
297
  }
298
+ // Normalize the working directory path to avoid Windows path duplication issues
299
+ const normalizedWorkingDir = path.resolve(conv.workingDirectory);
298
300
  // Create a fresh fsbrowse router for this conversation's directory
299
- const router = fsbrowse({ baseDir: conv.workingDirectory, name: 'Files' });
301
+ const router = fsbrowse({ baseDir: normalizedWorkingDir, name: 'Files' });
300
302
  // Strip the conversationId param from the path before passing to fsbrowse
301
303
  req.baseUrl = BASE_URL + '/files/' + req.params.conversationId;
302
304
  router(req, res, next);
@@ -1073,7 +1075,9 @@ const server = http.createServer(async (req, res) => {
1073
1075
 
1074
1076
  if (pathOnly === '/api/conversations' && req.method === 'POST') {
1075
1077
  const body = await parseBody(req);
1076
- const conversation = queries.createConversation(body.agentId, body.title, body.workingDirectory || null, body.model || null);
1078
+ // Normalize working directory to avoid Windows path issues
1079
+ const normalizedWorkingDir = body.workingDirectory ? path.resolve(body.workingDirectory) : null;
1080
+ const conversation = queries.createConversation(body.agentId, body.title, normalizedWorkingDir, body.model || null);
1077
1081
  queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory, model: conversation.model }, conversation.id);
1078
1082
  broadcastSync({ type: 'conversation_created', conversation });
1079
1083
  sendJSON(req, res, 201, { conversation });
@@ -1099,6 +1103,10 @@ const server = http.createServer(async (req, res) => {
1099
1103
 
1100
1104
  if (req.method === 'POST' || req.method === 'PUT') {
1101
1105
  const body = await parseBody(req);
1106
+ // Normalize working directory if present to avoid Windows path issues
1107
+ if (body.workingDirectory) {
1108
+ body.workingDirectory = path.resolve(body.workingDirectory);
1109
+ }
1102
1110
  const conv = queries.updateConversation(convMatch[1], body);
1103
1111
  if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
1104
1112
  queries.createEvent('conversation.updated', body, convMatch[1]);
@@ -420,8 +420,10 @@ class ConversationManager {
420
420
  const isStreaming = this.streamingConversations.has(conv.id);
421
421
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
422
422
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
423
+ const agent = this.getAgentDisplayName(conv.agentId || conv.agentType);
424
+ const modelLabel = conv.model ? ` (${conv.model})` : '';
423
425
  const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
424
- const metaParts = [timestamp];
426
+ const metaParts = [agent + modelLabel, timestamp];
425
427
  if (wd) metaParts.push(wd);
426
428
 
427
429
  const titleEl = el.querySelector('.conversation-item-title');
@@ -446,8 +448,10 @@ class ConversationManager {
446
448
 
447
449
  const title = conv.title || `Conversation ${conv.id.slice(0, 8)}`;
448
450
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
449
- const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
450
- const metaParts = [timestamp];
451
+ const agent = this.getAgentDisplayName(conv.agentId || conv.agentType);
452
+ const modelLabel = conv.model ? ` (${conv.model})` : '';
453
+ const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
454
+ const metaParts = [agent + modelLabel, timestamp];
451
455
  if (wd) metaParts.push(wd);
452
456
 
453
457
  const streamingBadge = isStreaming
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Wave 4 UI Consistency Test
4
+ * Tests agent/model persistence and display consolidation
5
+ */
6
+
7
+ import http from 'http';
8
+
9
+ const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
10
+ const API_BASE = `${BASE_URL}/gm/api`;
11
+
12
+ function request(method, path, body = null) {
13
+ return new Promise((resolve, reject) => {
14
+ const url = new URL(path, API_BASE);
15
+ const options = {
16
+ method,
17
+ headers: body ? { 'Content-Type': 'application/json' } : {}
18
+ };
19
+
20
+ const req = http.request(url, options, (res) => {
21
+ let data = '';
22
+ res.on('data', chunk => data += chunk);
23
+ res.on('end', () => {
24
+ try {
25
+ resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null });
26
+ } catch (e) {
27
+ resolve({ status: res.statusCode, data });
28
+ }
29
+ });
30
+ });
31
+
32
+ req.on('error', reject);
33
+ if (body) req.write(JSON.stringify(body));
34
+ req.end();
35
+ });
36
+ }
37
+
38
+ async function test() {
39
+ console.log('=== Wave 4 UI Consistency Tests ===\n');
40
+
41
+ try {
42
+ // Test 1: Create conversation with specific agent and model
43
+ console.log('Test 1: Create conversation with agent and model');
44
+ const createRes = await request('POST', '/conversations', {
45
+ agentId: 'claude-code',
46
+ title: 'Wave 4 Test Conversation',
47
+ workingDirectory: '/tmp/test',
48
+ model: 'claude-sonnet-4-5'
49
+ });
50
+
51
+ if (createRes.status !== 200) {
52
+ console.error('❌ Failed to create conversation:', createRes.status);
53
+ return;
54
+ }
55
+
56
+ const conversation = createRes.data.conversation;
57
+ console.log('✓ Created conversation:', conversation.id);
58
+ console.log(' - agentId:', conversation.agentId);
59
+ console.log(' - model:', conversation.model);
60
+
61
+ // Test 2: Fetch conversation and verify agent/model are returned
62
+ console.log('\nTest 2: Fetch conversation via /full endpoint');
63
+ const fullRes = await request('GET', `/conversations/${conversation.id}/full`);
64
+
65
+ if (fullRes.status !== 200) {
66
+ console.error('❌ Failed to fetch conversation:', fullRes.status);
67
+ return;
68
+ }
69
+
70
+ const fullConv = fullRes.data.conversation;
71
+ console.log('✓ Fetched conversation');
72
+ console.log(' - agentId:', fullConv.agentId);
73
+ console.log(' - agentType:', fullConv.agentType);
74
+ console.log(' - model:', fullConv.model);
75
+
76
+ if (!fullConv.agentId && !fullConv.agentType) {
77
+ console.error('❌ agentId/agentType missing from response');
78
+ } else {
79
+ console.log('✓ agentId/agentType present');
80
+ }
81
+
82
+ if (!fullConv.model) {
83
+ console.error('❌ model missing from response');
84
+ } else {
85
+ console.log('✓ model present');
86
+ }
87
+
88
+ // Test 3: List conversations and verify agent/model in list
89
+ console.log('\nTest 3: List conversations');
90
+ const listRes = await request('GET', '/conversations');
91
+
92
+ if (listRes.status !== 200) {
93
+ console.error('❌ Failed to list conversations:', listRes.status);
94
+ return;
95
+ }
96
+
97
+ const listedConv = listRes.data.conversations.find(c => c.id === conversation.id);
98
+ if (!listedConv) {
99
+ console.error('❌ Conversation not found in list');
100
+ return;
101
+ }
102
+
103
+ console.log('✓ Conversation in list');
104
+ console.log(' - agentId:', listedConv.agentId);
105
+ console.log(' - agentType:', listedConv.agentType);
106
+ console.log(' - model:', listedConv.model);
107
+
108
+ // Test 4: Update conversation model
109
+ console.log('\nTest 4: Update conversation model');
110
+ const updateRes = await request('POST', `/conversations/${conversation.id}`, {
111
+ model: 'claude-opus-4-6'
112
+ });
113
+
114
+ if (updateRes.status !== 200) {
115
+ console.error('❌ Failed to update conversation:', updateRes.status);
116
+ return;
117
+ }
118
+
119
+ const updatedConv = updateRes.data.conversation;
120
+ console.log('✓ Updated conversation');
121
+ console.log(' - model:', updatedConv.model);
122
+
123
+ if (updatedConv.model !== 'claude-opus-4-6') {
124
+ console.error('❌ Model not updated correctly');
125
+ } else {
126
+ console.log('✓ Model updated correctly');
127
+ }
128
+
129
+ // Cleanup
130
+ console.log('\nCleanup: Deleting test conversation');
131
+ await request('DELETE', `/conversations/${conversation.id}`);
132
+ console.log('✓ Deleted test conversation');
133
+
134
+ console.log('\n=== All Tests Passed ===');
135
+ } catch (error) {
136
+ console.error('❌ Test error:', error.message);
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ test();