agentgui 1.0.193 → 1.0.195

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.193",
3
+ "version": "1.0.195",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -666,6 +666,36 @@ const server = http.createServer(async (req, res) => {
666
666
  return;
667
667
  }
668
668
 
669
+ if (pathOnly === '/api/clone' && req.method === 'POST') {
670
+ const body = await parseBody(req);
671
+ const repo = (body.repo || '').trim();
672
+ if (!repo || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
673
+ sendJSON(req, res, 400, { error: 'Invalid repo format. Use org/repo or user/repo' });
674
+ return;
675
+ }
676
+ const cloneDir = STARTUP_CWD || os.homedir();
677
+ const repoName = repo.split('/')[1];
678
+ const targetPath = path.join(cloneDir, repoName);
679
+ if (fs.existsSync(targetPath)) {
680
+ sendJSON(req, res, 409, { error: `Directory already exists: ${repoName}`, path: targetPath });
681
+ return;
682
+ }
683
+ try {
684
+ execSync(`git clone https://github.com/${repo}.git`, {
685
+ cwd: cloneDir,
686
+ encoding: 'utf-8',
687
+ timeout: 120000,
688
+ stdio: ['pipe', 'pipe', 'pipe'],
689
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
690
+ });
691
+ sendJSON(req, res, 200, { ok: true, repo, path: targetPath, name: repoName });
692
+ } catch (err) {
693
+ const stderr = err.stderr || err.message || 'Clone failed';
694
+ sendJSON(req, res, 500, { error: stderr.trim() });
695
+ }
696
+ return;
697
+ }
698
+
669
699
  if (pathOnly === '/api/folders' && req.method === 'POST') {
670
700
  const body = await parseBody(req);
671
701
  const folderPath = body.path || STARTUP_CWD;
package/static/index.html CHANGED
@@ -118,6 +118,97 @@
118
118
 
119
119
  .sidebar-new-btn:hover { background-color: var(--color-primary-dark); }
120
120
 
121
+ .sidebar-header-actions {
122
+ display: flex;
123
+ gap: 0.375rem;
124
+ align-items: center;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ .sidebar-clone-btn {
129
+ padding: 0.375rem 0.625rem;
130
+ font-size: 0.75rem;
131
+ background-color: var(--color-bg-primary);
132
+ color: var(--color-text-primary);
133
+ border: 1px solid var(--color-border);
134
+ border-radius: 0.375rem;
135
+ cursor: pointer;
136
+ transition: all 0.2s;
137
+ white-space: nowrap;
138
+ flex-shrink: 0;
139
+ }
140
+
141
+ .sidebar-clone-btn:hover { border-color: var(--color-primary); color: var(--color-primary); }
142
+
143
+ .clone-input-bar {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 0.375rem;
147
+ padding: 0.375rem 0.75rem;
148
+ background: var(--color-bg-primary);
149
+ border-bottom: 1px solid var(--color-border);
150
+ flex-shrink: 0;
151
+ }
152
+
153
+ .clone-input {
154
+ flex: 1;
155
+ min-width: 0;
156
+ padding: 0.375rem 0.5rem;
157
+ font-size: 0.8rem;
158
+ font-family: 'Monaco','Menlo','Ubuntu Mono', monospace;
159
+ border: 1px solid var(--color-border);
160
+ border-radius: 0.25rem;
161
+ background: var(--color-bg-secondary);
162
+ color: var(--color-text-primary);
163
+ outline: none;
164
+ }
165
+
166
+ .clone-input:focus { border-color: var(--color-primary); }
167
+
168
+ .clone-go-btn {
169
+ padding: 0.375rem 0.625rem;
170
+ font-size: 0.75rem;
171
+ font-weight: 600;
172
+ background: var(--color-primary);
173
+ color: white;
174
+ border: none;
175
+ border-radius: 0.25rem;
176
+ cursor: pointer;
177
+ flex-shrink: 0;
178
+ transition: background-color 0.15s;
179
+ }
180
+
181
+ .clone-go-btn:hover { background-color: var(--color-primary-dark); }
182
+ .clone-go-btn:disabled { opacity: 0.5; cursor: not-allowed; }
183
+
184
+ .clone-cancel-btn {
185
+ padding: 0.25rem 0.5rem;
186
+ font-size: 1rem;
187
+ background: none;
188
+ border: none;
189
+ cursor: pointer;
190
+ color: var(--color-text-secondary);
191
+ flex-shrink: 0;
192
+ line-height: 1;
193
+ }
194
+
195
+ .clone-cancel-btn:hover { color: var(--color-text-primary); }
196
+
197
+ .clone-status {
198
+ padding: 0.375rem 0.75rem;
199
+ font-size: 0.75rem;
200
+ background: var(--color-bg-primary);
201
+ border-bottom: 1px solid var(--color-border);
202
+ flex-shrink: 0;
203
+ display: flex;
204
+ align-items: center;
205
+ gap: 0.5rem;
206
+ }
207
+
208
+ .clone-status.cloning { color: var(--color-primary); }
209
+ .clone-status.clone-error { color: var(--color-error); }
210
+ .clone-status.clone-success { color: var(--color-success); }
211
+
121
212
  .sidebar-list {
122
213
  flex: 1;
123
214
  overflow-y: auto;
@@ -829,7 +920,6 @@
829
920
  }
830
921
 
831
922
  .sidebar.collapsed { transform: translateX(-100%); width: var(--sidebar-width); }
832
- .sidebar:not(.collapsed),
833
923
  .sidebar.mobile-visible { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,0.2); }
834
924
 
835
925
  .main-header { padding: 0 0.75rem; }
@@ -2079,7 +2169,15 @@
2079
2169
  <aside class="sidebar" data-sidebar>
2080
2170
  <div class="sidebar-header">
2081
2171
  <h2>History</h2>
2082
- <button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
2172
+ <div class="sidebar-header-actions">
2173
+ <button id="cloneRepoBtn" class="sidebar-clone-btn" data-clone-repo title="Clone a GitHub repo">Clone</button>
2174
+ <button id="newConversationBtn" class="sidebar-new-btn" data-new-conversation title="Start new conversation">+ New</button>
2175
+ </div>
2176
+ </div>
2177
+ <div class="clone-input-bar" id="cloneInputBar" style="display:none;">
2178
+ <input type="text" class="clone-input" id="cloneRepoInput" placeholder="org/repo" autocomplete="off" spellcheck="false">
2179
+ <button class="clone-go-btn" id="cloneGoBtn" title="Clone">Go</button>
2180
+ <button class="clone-cancel-btn" id="cloneCancelBtn" title="Cancel">&times;</button>
2083
2181
  </div>
2084
2182
  <ul class="sidebar-list" data-conversation-list>
2085
2183
  <li class="sidebar-empty" data-conversation-empty>No conversations yet</li>
@@ -35,6 +35,7 @@ class ConversationManager {
35
35
  this.loadConversations();
36
36
  this.setupWebSocketListener();
37
37
  this.setupFolderBrowser();
38
+ this.setupCloneUI();
38
39
 
39
40
  setInterval(() => this.loadConversations(), 30000);
40
41
  }
@@ -210,6 +211,103 @@ class ConversationManager {
210
211
  }));
211
212
  }
212
213
 
214
+ setupCloneUI() {
215
+ this.cloneBtn = document.getElementById('cloneRepoBtn');
216
+ this.cloneBar = document.getElementById('cloneInputBar');
217
+ this.cloneInput = document.getElementById('cloneRepoInput');
218
+ this.cloneGoBtn = document.getElementById('cloneGoBtn');
219
+ this.cloneCancelBtn = document.getElementById('cloneCancelBtn');
220
+
221
+ if (!this.cloneBtn || !this.cloneBar) return;
222
+
223
+ this.cloneBtn.addEventListener('click', () => this.toggleCloneBar());
224
+
225
+ this.cloneCancelBtn?.addEventListener('click', () => this.hideCloneBar());
226
+
227
+ this.cloneGoBtn?.addEventListener('click', () => this.performClone());
228
+
229
+ this.cloneInput?.addEventListener('keydown', (e) => {
230
+ if (e.key === 'Enter') this.performClone();
231
+ if (e.key === 'Escape') this.hideCloneBar();
232
+ });
233
+ }
234
+
235
+ toggleCloneBar() {
236
+ if (!this.cloneBar) return;
237
+ const visible = this.cloneBar.style.display !== 'none';
238
+ if (visible) {
239
+ this.hideCloneBar();
240
+ } else {
241
+ this.cloneBar.style.display = 'flex';
242
+ this.cloneInput.value = '';
243
+ this.cloneInput.focus();
244
+ this.removeCloneStatus();
245
+ }
246
+ }
247
+
248
+ hideCloneBar() {
249
+ if (this.cloneBar) this.cloneBar.style.display = 'none';
250
+ this.removeCloneStatus();
251
+ }
252
+
253
+ removeCloneStatus() {
254
+ const existing = this.sidebarEl?.querySelector('.clone-status');
255
+ if (existing) existing.remove();
256
+ }
257
+
258
+ showCloneStatus(message, type) {
259
+ this.removeCloneStatus();
260
+ const statusEl = document.createElement('div');
261
+ statusEl.className = `clone-status ${type}`;
262
+ statusEl.textContent = message;
263
+ if (this.cloneBar && this.cloneBar.parentNode) {
264
+ this.cloneBar.parentNode.insertBefore(statusEl, this.cloneBar.nextSibling);
265
+ }
266
+ if (type === 'clone-success' || type === 'clone-error') {
267
+ setTimeout(() => statusEl.remove(), 5000);
268
+ }
269
+ }
270
+
271
+ async performClone() {
272
+ const repo = (this.cloneInput?.value || '').trim();
273
+ if (!repo) return;
274
+ if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo)) {
275
+ this.showCloneStatus('Invalid format. Use org/repo', 'clone-error');
276
+ return;
277
+ }
278
+
279
+ this.cloneGoBtn.disabled = true;
280
+ this.cloneInput.disabled = true;
281
+ this.showCloneStatus(`Cloning ${repo}...`, 'cloning');
282
+
283
+ try {
284
+ const res = await fetch((window.__BASE_URL || '') + '/api/clone', {
285
+ method: 'POST',
286
+ headers: { 'Content-Type': 'application/json' },
287
+ body: JSON.stringify({ repo })
288
+ });
289
+
290
+ const data = await res.json();
291
+
292
+ if (!res.ok) {
293
+ this.showCloneStatus(data.error || 'Clone failed', 'clone-error');
294
+ return;
295
+ }
296
+
297
+ this.showCloneStatus(`Cloned ${data.name}`, 'clone-success');
298
+ this.hideCloneBar();
299
+
300
+ window.dispatchEvent(new CustomEvent('create-new-conversation', {
301
+ detail: { workingDirectory: data.path, title: data.name }
302
+ }));
303
+ } catch (err) {
304
+ this.showCloneStatus('Network error: ' + err.message, 'clone-error');
305
+ } finally {
306
+ if (this.cloneGoBtn) this.cloneGoBtn.disabled = false;
307
+ if (this.cloneInput) this.cloneInput.disabled = false;
308
+ }
309
+ }
310
+
213
311
  async loadConversations() {
214
312
  try {
215
313
  const res = await fetch((window.__BASE_URL || '') + '/api/conversations');
@@ -23,9 +23,13 @@
23
23
 
24
24
  if (!sidebar) return;
25
25
 
26
- var savedState = localStorage.getItem('sidebar-collapsed');
27
- if (savedState === 'true' && window.innerWidth > 768) {
26
+ if (window.innerWidth <= 768) {
28
27
  sidebar.classList.add('collapsed');
28
+ } else {
29
+ var savedState = localStorage.getItem('sidebar-collapsed');
30
+ if (savedState === 'true') {
31
+ sidebar.classList.add('collapsed');
32
+ }
29
33
  }
30
34
 
31
35
  function isMobile() { return window.innerWidth <= 768; }