agentgui 1.0.45 → 1.0.47

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/.prd ADDED
@@ -0,0 +1,45 @@
1
+ # agentgui Work Items
2
+
3
+ ## ACTIVE 🔄
4
+
5
+ ### Phase 2: OpenCode Integration (PRIORITY 2)
6
+ **User Request:** Import all OpenCode conversations and keep them in sync
7
+
8
+ **Blockers:**
9
+ - [ ] Determine OpenCode conversation storage location
10
+
11
+ **Tasks:**
12
+ - [ ] Implement OpenCode history loader
13
+ - [ ] Merge both sources in database
14
+ - [ ] Display merged history in sidebar
15
+ - [ ] Support agent type filtering
16
+
17
+ ### Phase 3: Sync & Consistency (PRIORITY 3)
18
+ - [ ] File watchers for history changes
19
+ - [ ] Auto-import new conversations from CLI
20
+ - [ ] Keep GUI database in sync with filesystem
21
+ - [ ] Handle conflicts (same conversation in both systems)
22
+ - [ ] Implement read-only mode for imported conversations
23
+
24
+ ## ISSUES TO FIX ⚠️
25
+
26
+ **Issue #1: Timeout on Conversation Selection**
27
+ - Clicking conversations causes code execution timeout
28
+ - Needs investigation into click handlers
29
+
30
+ **Issue #2: Message Input Field**
31
+ - Message submission behavior unclear
32
+ - May not be properly wired to agent communication
33
+
34
+ ## Test Categories Remaining (10 of 12)
35
+ - [ ] Category 2: Real-Time Streaming with Persistence
36
+ - [ ] Category 3: HTML Rendering Without Text Mixing
37
+ - [ ] Category 4: Theme Compliance (Light/Dark)
38
+ - [ ] Category 5: Advanced RippleUI Components
39
+ - [ ] Category 6: Interactive Forms & Input Validation
40
+ - [ ] Category 7: Database Persistence & Recovery
41
+ - [ ] Category 8: State Consistency & Validation
42
+ - [ ] Category 9: Reconnection & Recovery
43
+ - [ ] Category 10: Error Handling & Edge Cases
44
+ - [ ] Category 11: Performance & Latency
45
+ - [ ] Category 12: Multi-Agent & Configuration
package/acp-launcher.js CHANGED
@@ -1,202 +1,75 @@
1
- import { createClient } from 'claude-code-acp';
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
1
+ import { query } from '@anthropic-ai/claude-code';
5
2
  import { default as SYSTEM_PROMPT } from './system-prompt.js';
6
3
 
7
- /**
8
- * Load CLI configuration to ensure identical behavior
9
- * Supports both Claude Code and OpenCode
10
- */
11
- function loadCLIConfig(agentType) {
12
- const configPaths = [
13
- // Claude Code paths
14
- path.join(os.homedir(), '.claude', 'config.json'),
15
- path.join(os.homedir(), '.claude-code', 'config.json'),
16
- path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'claude', 'config.json'),
17
- // OpenCode paths
18
- path.join(os.homedir(), '.opencode', 'config.json'),
19
- path.join(os.homedir(), '.config', 'opencode', 'config.json'),
20
- path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode', 'config.json')
21
- ];
22
-
23
- for (const configPath of configPaths) {
24
- try {
25
- if (fs.existsSync(configPath)) {
26
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
27
- console.log(`[ACP] Loaded ${agentType} CLI config from ${configPath}`);
28
- return config;
29
- }
30
- } catch (e) {
31
- // Config file doesn't exist or is invalid, continue
32
- }
33
- }
34
-
35
- console.log(`[ACP] No ${agentType} config found, using defaults`);
36
- return {};
37
- }
38
-
39
4
  export default class ACPConnection {
40
5
  constructor() {
41
- this.client = null;
42
6
  this.sessionId = null;
43
7
  this.onUpdate = null;
44
8
  }
45
9
 
46
- /**
47
- * Connect to ACP bridge and create session
48
- * Uses identical configuration to CLI version
49
- */
50
10
  async connect(agentType, cwd) {
51
- try {
52
- console.log(`[ACP] Connecting to ${agentType}...`);
53
-
54
- // Load CLI configuration for identical behavior
55
- const cliConfig = loadCLIConfig(agentType);
56
-
57
- // Create client with CLI-identical configuration
58
- // Pass through all environment for OAuth and plugin support
59
- const clientConfig = {
60
- agent: agentType === 'opencode' ? 'opencode' : 'claude-code',
61
- cwd,
62
- // Use same environment as CLI (HOME, PATH, etc.)
63
- env: process.env,
64
- // Load plugins just like CLI does
65
- plugins: true,
66
- // Use OAuth for authentication (same as CLI)
67
- oauth: true,
68
- // Use model preferences from CLI config
69
- modelPreferences: cliConfig.modelPreferences || undefined,
70
- // Enable all capabilities that CLI enables
71
- capabilities: {
72
- fs: true,
73
- mcp: true,
74
- web: true,
75
- terminal: true
76
- },
77
- // Pass through any other CLI settings
78
- ...cliConfig
79
- };
80
-
81
- // Remove potential conflicting fields
82
- delete clientConfig.agent; // Re-add below
83
- delete clientConfig.cwd; // Re-add below
84
-
85
- this.client = await createClient({
86
- agent: clientConfig.agent || (agentType === 'opencode' ? 'opencode' : 'claude-code'),
87
- cwd,
88
- ...clientConfig
89
- });
90
-
91
- console.log(`[ACP] ✅ Connected to ${agentType} (CLI-identical mode)`);
92
- } catch (err) {
93
- console.error(`[ACP] ❌ FATAL: Connection failed: ${err.message}`);
94
- throw new Error(`ACP connection failed for ${agentType}: ${err.message}`);
95
- }
11
+ console.log(`[ACP] Using Claude Code SDK (${agentType})`);
96
12
  }
97
13
 
98
- /**
99
- * Initialize ACP session
100
- */
101
14
  async initialize() {
102
- if (!this.client) throw new Error('ACP not connected');
103
- return this.client.request('initialize', {
104
- protocolVersion: 1,
105
- clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
106
- });
15
+ return { ready: true };
107
16
  }
108
17
 
109
- /**
110
- * Create new session
111
- */
112
18
  async newSession(cwd) {
113
- if (!this.client) throw new Error('ACP not connected');
114
- const result = await this.client.request('session/new', { cwd, mcpServers: [] });
115
- this.sessionId = result.sessionId;
116
- return result;
19
+ this.sessionId = Math.random().toString(36).substring(7);
20
+ return { sessionId: this.sessionId };
117
21
  }
118
22
 
119
- /**
120
- * Set session mode
121
- */
122
23
  async setSessionMode(modeId) {
123
- if (!this.client) throw new Error('ACP not connected');
124
- return this.client.request('session/set_mode', { sessionId: this.sessionId, modeId });
24
+ return { modeId };
125
25
  }
126
26
 
127
- /**
128
- * Inject unified HTML enforcement system prompt
129
- */
130
27
  async injectSkills(additionalContext = '') {
131
- if (!this.client) throw new Error('ACP not connected');
132
-
133
- const systemPrompt = additionalContext
134
- ? `${SYSTEM_PROMPT}\n\n---\n\n${additionalContext}`
135
- : SYSTEM_PROMPT;
136
-
137
- return this.client.request('session/skill_inject', {
138
- sessionId: this.sessionId,
139
- skills: [],
140
- notification: [{ type: 'text', text: systemPrompt }]
141
- });
28
+ return { skills: [] };
142
29
  }
143
30
 
144
- /**
145
- * Inject system context with unified HTML enforcement
146
- */
147
31
  async injectSystemContext() {
148
- if (!this.client) throw new Error('ACP not connected');
149
-
150
- return this.client.request('session/context', {
151
- sessionId: this.sessionId,
152
- context: SYSTEM_PROMPT,
153
- role: 'system'
154
- });
32
+ return { context: SYSTEM_PROMPT };
155
33
  }
156
34
 
157
- /**
158
- * Send prompt and stream updates
159
- */
160
35
  async sendPrompt(prompt) {
161
- if (!this.client) throw new Error('ACP not connected');
36
+ const messages = [];
37
+ let fullResponse = '';
162
38
 
163
- const promptContent = Array.isArray(prompt) ? prompt : [{ type: 'text', text: prompt }];
39
+ try {
40
+ const promptText = typeof prompt === 'string' ? prompt : prompt.map(p => p.text).join('\n');
41
+ const systemMessage = `${SYSTEM_PROMPT}\n\nUser Request: ${promptText}`;
164
42
 
165
- // Setup update handler before sending
166
- if (this.onUpdate) {
167
- this.client.on('update', (update) => {
168
- // Forward updates immediately with no delay
169
- this.onUpdate({ update });
43
+ const response = query({
44
+ prompt: systemMessage,
45
+ options: {}
170
46
  });
171
- }
172
47
 
173
- // Send prompt and get result
174
- return this.client.request('session/prompt', {
175
- sessionId: this.sessionId,
176
- prompt: promptContent
177
- }, 300000);
48
+ for await (const message of response) {
49
+ fullResponse += message.content?.map(c => c.text || '').join('') || '';
50
+
51
+ if (this.onUpdate) {
52
+ this.onUpdate({
53
+ update: {
54
+ sessionUpdate: 'agent_message_chunk',
55
+ content: { text: message.content?.map(c => c.text || '').join('') || '' }
56
+ }
57
+ });
58
+ }
59
+ }
60
+
61
+ return { content: fullResponse };
62
+ } catch (err) {
63
+ console.error(`[ACP] Query error: ${err.message}`);
64
+ throw err;
65
+ }
178
66
  }
179
67
 
180
- /**
181
- * Check if connection is running
182
- */
183
68
  isRunning() {
184
- return this.client !== null;
69
+ return true;
185
70
  }
186
71
 
187
- /**
188
- * Terminate connection
189
- */
190
72
  async terminate() {
191
- if (!this.client) return;
192
-
193
- try {
194
- await this.client.close();
195
- } catch (err) {
196
- console.error(`[ACP] Error during terminate: ${err.message}`);
197
- } finally {
198
- this.client = null;
199
- this.sessionId = null;
200
- }
73
+ return;
201
74
  }
202
75
  }
@@ -0,0 +1,70 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { queries } from './database.js';
5
+
6
+ export class ConversationImporter {
7
+ static async importClaudeCodeSessions() {
8
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
9
+ if (!fs.existsSync(projectsDir)) return [];
10
+
11
+ const imported = [];
12
+ const projects = fs.readdirSync(projectsDir);
13
+
14
+ for (const projectName of projects) {
15
+ const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
16
+ if (!fs.existsSync(indexPath)) continue;
17
+
18
+ try {
19
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
20
+ const entries = index.entries || [];
21
+
22
+ for (const entry of entries) {
23
+ try {
24
+ const existing = queries.getConversationByExternalId('claude-code', entry.sessionId);
25
+ if (existing) continue;
26
+
27
+ const conversation = {
28
+ externalId: entry.sessionId,
29
+ agentType: 'claude-code',
30
+ title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
31
+ firstPrompt: entry.firstPrompt,
32
+ messageCount: entry.messageCount || 0,
33
+ created: new Date(entry.created).getTime(),
34
+ modified: new Date(entry.modified).getTime(),
35
+ projectPath: entry.projectPath,
36
+ gitBranch: entry.gitBranch,
37
+ sourcePath: entry.fullPath,
38
+ source: 'imported'
39
+ };
40
+
41
+ queries.createImportedConversation(conversation);
42
+ imported.push(conversation);
43
+ } catch (err) {
44
+ console.error(`[Importer] Error importing session ${entry.sessionId}:`, err.message);
45
+ }
46
+ }
47
+ } catch (err) {
48
+ console.error(`[Importer] Error reading ${indexPath}:`, err.message);
49
+ }
50
+ }
51
+
52
+ return imported;
53
+ }
54
+
55
+ static async importOpenCodeSessions() {
56
+ // TODO: Implement OpenCode session import once storage location is determined
57
+ return [];
58
+ }
59
+
60
+ static async importAll() {
61
+ console.log('[Importer] Starting conversation import...');
62
+ const claudeCode = await this.importClaudeCodeSessions();
63
+ const openCode = await this.importOpenCodeSessions();
64
+ console.log(`[Importer] Imported ${claudeCode.length} Claude Code conversations`);
65
+ console.log(`[Importer] Imported ${openCode.length} OpenCode conversations`);
66
+ return { claudeCode, openCode };
67
+ }
68
+ }
69
+
70
+ export default ConversationImporter;
package/database.js CHANGED
@@ -30,6 +30,7 @@ try {
30
30
  }
31
31
 
32
32
  function initSchema() {
33
+ // Create table with minimal schema - columns will be added by migration
33
34
  db.exec(`
34
35
  CREATE TABLE IF NOT EXISTS conversations (
35
36
  id TEXT PRIMARY KEY,
@@ -183,6 +184,45 @@ function migrateFromJson() {
183
184
  initSchema();
184
185
  migrateFromJson();
185
186
 
187
+ // Migration: Add imported conversation columns if they don't exist
188
+ try {
189
+ const result = db.prepare("PRAGMA table_info(conversations)").all();
190
+ const columnNames = result.map(r => r.name);
191
+ const requiredColumns = {
192
+ agentType: 'TEXT DEFAULT "claude-code"',
193
+ source: 'TEXT DEFAULT "gui"',
194
+ externalId: 'TEXT',
195
+ firstPrompt: 'TEXT',
196
+ messageCount: 'INTEGER DEFAULT 0',
197
+ projectPath: 'TEXT',
198
+ gitBranch: 'TEXT',
199
+ sourcePath: 'TEXT',
200
+ lastSyncedAt: 'INTEGER'
201
+ };
202
+
203
+ let addedColumns = false;
204
+ for (const [colName, colDef] of Object.entries(requiredColumns)) {
205
+ if (!columnNames.includes(colName)) {
206
+ db.exec(`ALTER TABLE conversations ADD COLUMN ${colName} ${colDef}`);
207
+ console.log(`[Migration] Added column ${colName} to conversations table`);
208
+ addedColumns = true;
209
+ }
210
+ }
211
+
212
+ // Add indexes for new columns
213
+ if (addedColumns) {
214
+ try {
215
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_external ON conversations(externalId)`);
216
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_agent_type ON conversations(agentType)`);
217
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_conversations_source ON conversations(source)`);
218
+ } catch (e) {
219
+ console.warn('[Migration] Index creation warning:', e.message);
220
+ }
221
+ }
222
+ } catch (err) {
223
+ console.error('[Migration] Error:', err.message);
224
+ }
225
+
186
226
  function generateId(prefix) {
187
227
  return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
188
228
  }
@@ -660,6 +700,111 @@ export const queries = {
660
700
  clearSessionStreamUpdates(sessionId) {
661
701
  const stmt = db.prepare('DELETE FROM stream_updates WHERE sessionId = ?');
662
702
  stmt.run(sessionId);
703
+ },
704
+
705
+ createImportedConversation(data) {
706
+ const id = generateId('conv');
707
+ const now = Date.now();
708
+ const stmt = db.prepare(
709
+ `INSERT INTO conversations (
710
+ id, agentId, title, created_at, updated_at, status,
711
+ agentType, source, externalId, firstPrompt, messageCount,
712
+ projectPath, gitBranch, sourcePath, lastSyncedAt
713
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
714
+ );
715
+ stmt.run(
716
+ id,
717
+ data.externalId || id,
718
+ data.title,
719
+ data.created || now,
720
+ data.modified || now,
721
+ 'active',
722
+ data.agentType || 'claude-code',
723
+ data.source || 'imported',
724
+ data.externalId,
725
+ data.firstPrompt,
726
+ data.messageCount || 0,
727
+ data.projectPath,
728
+ data.gitBranch,
729
+ data.sourcePath,
730
+ now
731
+ );
732
+ return { id, ...data };
733
+ },
734
+
735
+ getConversationByExternalId(agentType, externalId) {
736
+ const stmt = db.prepare(
737
+ 'SELECT * FROM conversations WHERE agentType = ? AND externalId = ?'
738
+ );
739
+ return stmt.get(agentType, externalId);
740
+ },
741
+
742
+ getConversationsByAgentType(agentType) {
743
+ const stmt = db.prepare(
744
+ 'SELECT * FROM conversations WHERE agentType = ? AND status != ? ORDER BY updated_at DESC'
745
+ );
746
+ return stmt.all(agentType, 'deleted');
747
+ },
748
+
749
+ getImportedConversations() {
750
+ const stmt = db.prepare(
751
+ 'SELECT * FROM conversations WHERE source = ? AND status != ? ORDER BY updated_at DESC'
752
+ );
753
+ return stmt.all('imported', 'deleted');
754
+ },
755
+
756
+ importClaudeCodeConversations() {
757
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects');
758
+ if (!fs.existsSync(projectsDir)) return [];
759
+
760
+ const imported = [];
761
+ const projects = fs.readdirSync(projectsDir);
762
+
763
+ for (const projectName of projects) {
764
+ const indexPath = path.join(projectsDir, projectName, 'sessions-index.json');
765
+ if (!fs.existsSync(indexPath)) continue;
766
+
767
+ try {
768
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
769
+ const entries = index.entries || [];
770
+
771
+ for (const entry of entries) {
772
+ try {
773
+ const existing = this.getConversationByExternalId('claude-code', entry.sessionId);
774
+ if (existing) {
775
+ imported.push({ status: 'skipped', id: existing.id });
776
+ continue;
777
+ }
778
+
779
+ this.createImportedConversation({
780
+ externalId: entry.sessionId,
781
+ agentType: 'claude-code',
782
+ title: entry.summary || entry.firstPrompt || `Conversation ${entry.sessionId.slice(0, 8)}`,
783
+ firstPrompt: entry.firstPrompt,
784
+ messageCount: entry.messageCount || 0,
785
+ created: new Date(entry.created).getTime(),
786
+ modified: new Date(entry.modified).getTime(),
787
+ projectPath: entry.projectPath,
788
+ gitBranch: entry.gitBranch,
789
+ sourcePath: entry.fullPath,
790
+ source: 'imported'
791
+ });
792
+
793
+ imported.push({
794
+ status: 'imported',
795
+ id: entry.sessionId,
796
+ title: entry.summary || entry.firstPrompt
797
+ });
798
+ } catch (err) {
799
+ console.error(`[DB] Error importing session ${entry.sessionId}:`, err.message);
800
+ }
801
+ }
802
+ } catch (err) {
803
+ console.error(`[DB] Error reading ${indexPath}:`, err.message);
804
+ }
805
+ }
806
+
807
+ return imported;
663
808
  }
664
809
  };
665
810
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -21,8 +21,8 @@
21
21
  "dev": "node server.js --watch"
22
22
  },
23
23
  "dependencies": {
24
+ "@anthropic-ai/claude-code": "^1.0.128",
24
25
  "better-sqlite3": "^12.6.2",
25
- "claude-code-acp": "^1.0.0",
26
26
  "ws": "^8.14.2"
27
27
  }
28
28
  }