agentgui 1.0.819 → 1.0.821

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,42 @@
1
+ [
2
+ {
3
+ "id": "split-conversations-js",
4
+ "subject": "Split conversations.js into 3 files under 200 lines each",
5
+ "status": "pending",
6
+ "description": "conversations.js is 742L. Natural split: (1) conv-folder-browser.js — FolderBrowser methods L189-470 (~90L), (2) conv-list.js — ConversationManager render/CRUD/WS methods L480-742 (~180L), (3) conversations.js — core class constructor/init/loadAgents/agents/format methods L1-190 (~190L). All three reference shared ConversationManager instance.",
7
+ "effort": "large",
8
+ "category": "refactor",
9
+ "blocking": [],
10
+ "blockedBy": [],
11
+ "acceptance": [
12
+ "conversations.js ≤200 lines",
13
+ "conv-folder-browser.js ≤200 lines and loaded in index.html",
14
+ "conv-list.js ≤200 lines and loaded in index.html",
15
+ "window.conversationManager still works post-split",
16
+ "DOM-ready guard at end of file preserved (if/else is correct, not a duplicate)"
17
+ ],
18
+ "edge_cases": [
19
+ "Methods cross-call — must verify all internal references resolve after split"
20
+ ]
21
+ },
22
+ {
23
+ "id": "split-websocket-manager",
24
+ "subject": "Split websocket-manager.js into connection and messaging files",
25
+ "status": "pending",
26
+ "description": "websocket-manager.js is 650L with a single WebSocketManager class. Split into: (1) ws-connection.js — raw WebSocket open/close/reconnect logic, (2) ws-subscriptions.js — subscribe/unsubscribe/broadcast logic, (3) websocket-manager.js — coordinator that imports both (≤200L each).",
27
+ "effort": "large",
28
+ "category": "refactor",
29
+ "blocking": [],
30
+ "blockedBy": [],
31
+ "acceptance": [
32
+ "websocket-manager.js ≤200 lines",
33
+ "ws-connection.js ≤200 lines",
34
+ "ws-subscriptions.js ≤200 lines",
35
+ "All three loaded in index.html in correct order",
36
+ "wsManager API unchanged from client.js perspective"
37
+ ],
38
+ "edge_cases": [
39
+ "ws-machine.js wraps WebSocketManager — must still find it at window.wsManager"
40
+ ]
41
+ }
42
+ ]
package/CHANGELOG.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ## 2026-04-11
2
2
  - refactor: split jsonl-watcher.js parse/event logic into jsonl-parser.js; watcher retains file watching/polling only
3
3
  - refactor: split tool-version.js into tool-version-check.js (sync) and tool-version-fetch.js (async/network)
4
+ - refactor: split ui-components.js into core class (createModal/createTabs/createAlert/createSpinner/escapeHtml) and ui-components-rendering.js (createProgressBar/createCollapsible/createInput/createSelect/createButtonGroup/createBadge/copyToClipboard/downloadFile); both ≤200 lines
4
5
 
5
6
  ## [Unreleased]
6
7
  ### Refactor
package/CLAUDE.md CHANGED
@@ -88,7 +88,8 @@ static/js/stt-handler.js Speech-to-text recording and upload
88
88
  static/js/features.js View toggle, drag-drop upload, model progress indicator
89
89
  static/js/tools-manager.js Tool install/update UI orchestrator
90
90
  static/js/tools-manager-ui.js Tool card rendering + voice selector helpers
91
- static/js/agent-auth.js Agent authentication UI (OAuth flows)
91
+ static/js/agent-auth.js Agent authentication UI (dropdown, auth-status, provider keys)
92
+ static/js/agent-auth-oauth.js OAuth modal functions (triggerAuth, onWsMessage, paste fallback)
92
93
  static/js/dialogs.js Modal dialog system
93
94
  static/js/image-loader.js Lazy image loading for agent file read events
94
95
  static/js/pm2-monitor.js PM2 process monitor UI
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.819",
3
+ "version": "1.0.821",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/static/index.html CHANGED
@@ -293,12 +293,14 @@
293
293
  <script defer src="/gm/js/recording-machine.js"></script>
294
294
  <script defer src="/gm/js/terminal-machine.js"></script>
295
295
  <script defer src="/gm/js/conversations.js"></script>
296
+ <script defer src="/gm/js/conv-sidebar-actions.js"></script>
296
297
  <script defer src="/gm/lib/msgpackr.min.js"></script>
297
298
  <script defer src="/gm/js/websocket-manager.js"></script>
298
299
  <script defer src="/gm/js/ws-client.js"></script>
299
300
  <script defer src="/gm/js/syntax-highlighter.js"></script>
300
301
  <script defer src="/gm/js/dialogs.js"></script>
301
302
  <script defer src="/gm/js/ui-components.js"></script>
303
+ <script defer src="/gm/js/ui-components-rendering.js"></script>
302
304
  <script defer src="/gm/js/state-barrier.js"></script>
303
305
  <script defer src="/gm/js/terminal.js"></script>
304
306
  <script defer src="/gm/js/script-runner.js"></script>
@@ -307,9 +309,12 @@
307
309
  <script defer src="/gm/js/stt-handler.js"></script>
308
310
  <script defer src="/gm/js/voice.js"></script>
309
311
  <script defer src="/gm/js/pm2-monitor.js"></script>
312
+ <script defer src="/gm/js/event-filter-config.js"></script>
313
+ <script defer src="/gm/js/event-filter.js"></script>
310
314
  <script defer src="/gm/js/client.js"></script>
311
315
  <script defer src="/gm/js/features.js"></script>
312
316
  <script defer src="/gm/js/agent-auth.js"></script>
317
+ <script defer src="/gm/js/agent-auth-oauth.js"></script>
313
318
  <script defer src="/gm/js/app-shortcuts.js"></script>
314
319
  <script defer src="/gm/app.js"></script>
315
320
 
@@ -0,0 +1,159 @@
1
+ (function() {
2
+ var AUTH_CONV_ID = '__agent_auth__';
3
+ var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
4
+
5
+ function state() { return window.__agentAuthState; }
6
+
7
+ function cleanupOAuthPolling() {
8
+ if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
9
+ if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
10
+ if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
11
+ }
12
+
13
+ function showOAuthWaitingModal() {
14
+ removeOAuthModal();
15
+ var overlay = document.createElement('div');
16
+ overlay.id = 'oauthWaitingModal';
17
+ overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
18
+ overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
19
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
20
+ '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Google Sign-In</h2>' +
21
+ '<button id="oauthWaitingClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
22
+ '<div id="oauthWaitingContent" style="text-align:center;padding:1.5rem 0;">' +
23
+ '<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">&#9203;</div>' +
24
+ '<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">Waiting for Google sign-in to complete...</p>' +
25
+ '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">Complete the sign-in in the tab that just opened.</p>' +
26
+ '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0.25rem 0 0;">This dialog will close automatically when done.</p></div>' +
27
+ '<div id="oauthPasteFallback" style="display:none;">' +
28
+ '<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
29
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">The automatic relay did not complete. This can happen when accessing the server remotely.</p>' +
30
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0;">Copy the <span style="color:white;font-weight:600;">entire URL</span> from the sign-in tab and paste it below.</p></div>' +
31
+ '<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
32
+ '<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p></div>' +
33
+ '<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
34
+ '<button id="oauthWaitingCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
35
+ '<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;display:none;">Complete Sign-In</button></div>' +
36
+ '<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
37
+ document.body.appendChild(overlay);
38
+ var dismiss = function() { cleanupOAuthPolling(); state().authRunning = false; removeOAuthModal(); };
39
+ document.getElementById('oauthWaitingClose').addEventListener('click', dismiss);
40
+ document.getElementById('oauthWaitingCancel').addEventListener('click', dismiss);
41
+ document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
42
+ }
43
+
44
+ function showOAuthPasteFallback() {
45
+ var fallback = document.getElementById('oauthPasteFallback');
46
+ var waitContent = document.getElementById('oauthWaitingContent');
47
+ var submitBtn = document.getElementById('oauthPasteSubmit');
48
+ if (fallback) fallback.style.display = 'block';
49
+ if (waitContent) waitContent.style.display = 'none';
50
+ if (submitBtn) submitBtn.style.display = 'block';
51
+ var input = document.getElementById('oauthPasteInput');
52
+ if (input) {
53
+ input.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
54
+ setTimeout(function() { input.focus(); }, 100);
55
+ }
56
+ }
57
+
58
+ function removeOAuthModal() {
59
+ var el = document.getElementById('oauthWaitingModal');
60
+ if (el) el.remove();
61
+ }
62
+
63
+ function submitOAuthPasteUrl() {
64
+ var input = document.getElementById('oauthPasteInput');
65
+ var errorEl = document.getElementById('oauthPasteError');
66
+ var submitBtn = document.getElementById('oauthPasteSubmit');
67
+ if (!input) return;
68
+ var url = input.value.trim();
69
+ if (!url) {
70
+ if (errorEl) { errorEl.textContent = 'Please paste the URL from the redirected page.'; errorEl.style.display = 'block'; }
71
+ return;
72
+ }
73
+ if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; }
74
+ if (errorEl) errorEl.style.display = 'none';
75
+ window.wsClient.rpc('gemini.complete', { url: url })
76
+ .then(function(data) {
77
+ if (data.success) {
78
+ cleanupOAuthPolling();
79
+ state().authRunning = false;
80
+ removeOAuthModal();
81
+ state().refresh();
82
+ } else {
83
+ if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
84
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
85
+ }
86
+ }).catch(function(e) {
87
+ if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
88
+ if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
89
+ });
90
+ }
91
+
92
+ function triggerAuth(agentId) {
93
+ if (state().authRunning) return;
94
+ window.wsClient.rpc('agent.auth', { id: agentId })
95
+ .then(function(data) {
96
+ if (data.ok) {
97
+ state().authRunning = true; showTerminalTab(); switchToTerminalView();
98
+ var term = getTerminal();
99
+ if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
100
+ if (data.authUrl) {
101
+ window.open(data.authUrl, '_blank');
102
+ if (agentId === 'gemini') {
103
+ showOAuthWaitingModal();
104
+ cleanupOAuthPolling();
105
+ oauthPollInterval = setInterval(function() {
106
+ window.wsClient.rpc('gemini.status')
107
+ .then(function(status) {
108
+ if (status.status === 'success') {
109
+ cleanupOAuthPolling(); state().authRunning = false; removeOAuthModal(); state().refresh();
110
+ } else if (status.status === 'error') {
111
+ cleanupOAuthPolling(); state().authRunning = false; removeOAuthModal();
112
+ }
113
+ }).catch(function() {});
114
+ }, 1500);
115
+ oauthFallbackTimer = setTimeout(function() {
116
+ if (state().authRunning) showOAuthPasteFallback();
117
+ }, 30000);
118
+ oauthPollTimeout = setTimeout(function() {
119
+ cleanupOAuthPolling();
120
+ if (state().authRunning) { state().authRunning = false; removeOAuthModal(); }
121
+ }, 5 * 60 * 1000);
122
+ }
123
+ }
124
+ }
125
+ }).catch(function() {});
126
+ }
127
+
128
+ function onWsMessage(e) {
129
+ var data = e.detail;
130
+ if (!data || data.conversationId !== AUTH_CONV_ID) return;
131
+ if (data.type === 'script_started') {
132
+ state().authRunning = true; showTerminalTab(); switchToTerminalView();
133
+ var term = getTerminal();
134
+ if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n'); }
135
+ } else if (data.type === 'script_output') {
136
+ showTerminalTab();
137
+ var term = getTerminal();
138
+ if (term) term.write(data.data);
139
+ } else if (data.type === 'script_stopped') {
140
+ state().authRunning = false;
141
+ removeOAuthModal();
142
+ cleanupOAuthPolling();
143
+ var term = getTerminal();
144
+ var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
145
+ if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
146
+ setTimeout(function() { state().refresh(); }, 1000);
147
+ }
148
+ }
149
+
150
+ function showTerminalTab() { var t = document.getElementById('terminalTabBtn'); if (t) t.style.display = ''; }
151
+ function switchToTerminalView() {
152
+ var bar = document.getElementById('viewToggleBar');
153
+ if (!bar) return;
154
+ var t = bar.querySelector('[data-view="terminal"]'); if (t) t.click();
155
+ }
156
+ function getTerminal() { return window.scriptRunner ? window.scriptRunner.getTerminal() : null; }
157
+
158
+ window.__agentAuthOAuth = { triggerAuth: triggerAuth, onWsMessage: onWsMessage };
159
+ })();
@@ -2,7 +2,13 @@
2
2
  var btn = document.getElementById('agentAuthBtn');
3
3
  var dropdown = document.getElementById('agentAuthDropdown');
4
4
  var agents = [], providers = {}, authRunning = false, editingProvider = null;
5
- var AUTH_CONV_ID = '__agent_auth__';
5
+
6
+ window.__agentAuthState = {
7
+ get authRunning() { return authRunning; },
8
+ set authRunning(v) { authRunning = v; },
9
+ refresh: function() { refresh(); },
10
+ closeDropdown: function() { closeDropdown(); }
11
+ };
6
12
 
7
13
  function init() {
8
14
  if (!btn || !dropdown) return;
@@ -12,7 +18,9 @@
12
18
  if (!btn.contains(e.target) && !dropdown.contains(e.target)) closeDropdown();
13
19
  });
14
20
  window.addEventListener('conversation-selected', function() { refresh(); });
15
- window.addEventListener('ws-message', onWsMessage);
21
+ window.addEventListener('ws-message', function(e) {
22
+ if (window.__agentAuthOAuth && window.__agentAuthOAuth.onWsMessage) window.__agentAuthOAuth.onWsMessage(e);
23
+ });
16
24
  refresh();
17
25
  }
18
26
 
@@ -48,7 +56,11 @@
48
56
  agents.forEach(function(agent) {
49
57
  var dotClass = agent.authenticated ? 'ok' : (agent.detail === 'unknown' ? 'unknown' : 'missing');
50
58
  var item = makeItem(dotClass, agent.name, agent.detail);
51
- item.addEventListener('click', function(e) { e.stopPropagation(); closeDropdown(); triggerAuth(agent.id); });
59
+ item.addEventListener('click', function(e) {
60
+ e.stopPropagation();
61
+ closeDropdown();
62
+ if (window.__agentAuthOAuth && window.__agentAuthOAuth.triggerAuth) window.__agentAuthOAuth.triggerAuth(agent.id);
63
+ });
52
64
  dropdown.appendChild(item);
53
65
  });
54
66
  }
@@ -126,165 +138,6 @@
126
138
  }
127
139
 
128
140
  function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
129
-
130
- var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
131
-
132
- function cleanupOAuthPolling() {
133
- if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
134
- if (oauthPollTimeout) { clearTimeout(oauthPollTimeout); oauthPollTimeout = null; }
135
- if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
136
- }
137
-
138
- function showOAuthWaitingModal() {
139
- removeOAuthModal();
140
- var overlay = document.createElement('div');
141
- overlay.id = 'oauthWaitingModal';
142
- overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
143
- overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:28rem;width:calc(100% - 2rem);box-shadow:0 25px 50px rgba(0,0,0,0.5);color:var(--color-text-primary,white);font-family:system-ui,sans-serif;" onclick="event.stopPropagation()">' +
144
- '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">' +
145
- '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Google Sign-In</h2>' +
146
- '<button id="oauthWaitingClose" style="background:none;border:none;color:var(--color-text-secondary,#9ca3af);font-size:1.5rem;cursor:pointer;padding:0;line-height:1;">\u00d7</button></div>' +
147
- '<div id="oauthWaitingContent" style="text-align:center;padding:1.5rem 0;">' +
148
- '<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">&#9203;</div>' +
149
- '<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">Waiting for Google sign-in to complete...</p>' +
150
- '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">Complete the sign-in in the tab that just opened.</p>' +
151
- '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0.25rem 0 0;">This dialog will close automatically when done.</p></div>' +
152
- '<div id="oauthPasteFallback" style="display:none;">' +
153
- '<div style="margin-bottom:1rem;padding:1rem;background:var(--color-bg-tertiary,rgba(255,255,255,0.05));border-radius:0.5rem;">' +
154
- '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 0.5rem;">The automatic relay did not complete. This can happen when accessing the server remotely.</p>' +
155
- '<p style="font-size:0.8rem;color:var(--color-text-secondary,#d1d5db);margin:0;">Copy the <span style="color:white;font-weight:600;">entire URL</span> from the sign-in tab and paste it below.</p></div>' +
156
- '<input type="text" id="oauthPasteInput" placeholder="http://localhost:3000/gm/oauth2callback?code=..." style="width:100%;box-sizing:border-box;padding:0.75rem 1rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.5rem;color:var(--color-text-primary,white);font-size:0.8rem;font-family:monospace;outline:none;" />' +
157
- '<p id="oauthPasteError" style="font-size:0.75rem;color:#ef4444;margin:0.5rem 0 0;display:none;"></p></div>' +
158
- '<div style="display:flex;gap:0.75rem;margin-top:1.25rem;">' +
159
- '<button id="oauthWaitingCancel" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:1px solid var(--color-border,#4b5563);background:transparent;color:var(--color-text-primary,white);font-size:0.8rem;cursor:pointer;font-weight:600;">Cancel</button>' +
160
- '<button id="oauthPasteSubmit" style="flex:1;padding:0.625rem;border-radius:0.5rem;border:none;background:var(--color-primary,#3b82f6);color:white;font-size:0.8rem;cursor:pointer;font-weight:600;display:none;">Complete Sign-In</button></div>' +
161
- '<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
162
- document.body.appendChild(overlay);
163
- var dismiss = function() { cleanupOAuthPolling(); authRunning = false; removeOAuthModal(); };
164
- document.getElementById('oauthWaitingClose').addEventListener('click', dismiss);
165
- document.getElementById('oauthWaitingCancel').addEventListener('click', dismiss);
166
- document.getElementById('oauthPasteSubmit').addEventListener('click', submitOAuthPasteUrl);
167
- }
168
-
169
- function showOAuthPasteFallback() {
170
- var fallback = document.getElementById('oauthPasteFallback');
171
- var waitContent = document.getElementById('oauthWaitingContent');
172
- var submitBtn = document.getElementById('oauthPasteSubmit');
173
- if (fallback) fallback.style.display = 'block';
174
- if (waitContent) waitContent.style.display = 'none';
175
- if (submitBtn) submitBtn.style.display = 'block';
176
- var input = document.getElementById('oauthPasteInput');
177
- if (input) {
178
- input.addEventListener('keydown', function(e) { if (e.key === 'Enter') submitOAuthPasteUrl(); });
179
- setTimeout(function() { input.focus(); }, 100);
180
- }
181
- }
182
-
183
- function removeOAuthModal() {
184
- var el = document.getElementById('oauthWaitingModal');
185
- if (el) el.remove();
186
- }
187
-
188
- function submitOAuthPasteUrl() {
189
- var input = document.getElementById('oauthPasteInput');
190
- var errorEl = document.getElementById('oauthPasteError');
191
- var submitBtn = document.getElementById('oauthPasteSubmit');
192
- if (!input) return;
193
- var url = input.value.trim();
194
- if (!url) {
195
- if (errorEl) { errorEl.textContent = 'Please paste the URL from the redirected page.'; errorEl.style.display = 'block'; }
196
- return;
197
- }
198
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Verifying...'; }
199
- if (errorEl) errorEl.style.display = 'none';
200
-
201
- window.wsClient.rpc('gemini.complete', { url: url })
202
- .then(function(data) {
203
- if (data.success) {
204
- cleanupOAuthPolling();
205
- authRunning = false;
206
- removeOAuthModal();
207
- refresh();
208
- } else {
209
- if (errorEl) { errorEl.textContent = data.error || 'Failed to complete authentication.'; errorEl.style.display = 'block'; }
210
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
211
- }
212
- }).catch(function(e) {
213
- if (errorEl) { errorEl.textContent = e.message; errorEl.style.display = 'block'; }
214
- if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Complete Sign-In'; }
215
- });
216
- }
217
-
218
- function triggerAuth(agentId) {
219
- if (authRunning) return;
220
- window.wsClient.rpc('agent.auth', { id: agentId })
221
- .then(function(data) {
222
- if (data.ok) {
223
- authRunning = true; showTerminalTab(); switchToTerminalView();
224
- var term = getTerminal();
225
- if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + agentId + ']\x1b[0m\r\n'); }
226
- if (data.authUrl) {
227
- window.open(data.authUrl, '_blank');
228
- if (agentId === 'gemini') {
229
- showOAuthWaitingModal();
230
- cleanupOAuthPolling();
231
- oauthPollInterval = setInterval(function() {
232
- window.wsClient.rpc('gemini.status')
233
- .then(function(status) {
234
- if (status.status === 'success') {
235
- cleanupOAuthPolling();
236
- authRunning = false;
237
- removeOAuthModal();
238
- refresh();
239
- } else if (status.status === 'error') {
240
- cleanupOAuthPolling();
241
- authRunning = false;
242
- removeOAuthModal();
243
- }
244
- }).catch(function() {});
245
- }, 1500);
246
- oauthFallbackTimer = setTimeout(function() {
247
- if (authRunning) showOAuthPasteFallback();
248
- }, 30000);
249
- oauthPollTimeout = setTimeout(function() {
250
- cleanupOAuthPolling();
251
- if (authRunning) { authRunning = false; removeOAuthModal(); }
252
- }, 5 * 60 * 1000);
253
- }
254
- }
255
- }
256
- }).catch(function() {});
257
- }
258
-
259
- function onWsMessage(e) {
260
- var data = e.detail;
261
- if (!data || data.conversationId !== AUTH_CONV_ID) return;
262
- if (data.type === 'script_started') {
263
- authRunning = true; showTerminalTab(); switchToTerminalView();
264
- var term = getTerminal();
265
- if (term) { term.clear(); term.writeln('\x1b[36m[authenticating ' + (data.agentId || '') + ']\x1b[0m\r\n'); }
266
- } else if (data.type === 'script_output') {
267
- showTerminalTab();
268
- var term = getTerminal();
269
- if (term) term.write(data.data);
270
- } else if (data.type === 'script_stopped') {
271
- authRunning = false;
272
- removeOAuthModal();
273
- cleanupOAuthPolling();
274
- var term = getTerminal();
275
- var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
276
- if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');
277
- setTimeout(refresh, 1000);
278
- }
279
- }
280
-
281
- function showTerminalTab() { var t = document.getElementById('terminalTabBtn'); if (t) t.style.display = ''; }
282
- function switchToTerminalView() {
283
- var bar = document.getElementById('viewToggleBar');
284
- if (!bar) return;
285
- var t = bar.querySelector('[data-view="terminal"]'); if (t) t.click();
286
- }
287
- function getTerminal() { return window.scriptRunner ? window.scriptRunner.getTerminal() : null; }
288
141
  function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
289
142
 
290
143
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
@@ -0,0 +1,184 @@
1
+ Object.assign(ConversationManager.prototype, {
2
+ setupDelegatedListeners() {
3
+ let draggedId = null;
4
+ this.listEl.addEventListener('dragstart', (e) => {
5
+ const item = e.target.closest('[data-drag-conv]');
6
+ if (!item) return;
7
+ draggedId = item.dataset.dragConv;
8
+ item.style.opacity = '0.5';
9
+ e.dataTransfer.effectAllowed = 'move';
10
+ });
11
+ this.listEl.addEventListener('dragend', (e) => {
12
+ const item = e.target.closest('[data-drag-conv]');
13
+ if (item) item.style.opacity = '';
14
+ draggedId = null;
15
+ });
16
+ this.listEl.addEventListener('dragover', (e) => {
17
+ const item = e.target.closest('[data-drag-conv]');
18
+ if (item && draggedId) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
19
+ });
20
+ this.listEl.addEventListener('drop', (e) => {
21
+ e.preventDefault();
22
+ const target = e.target.closest('[data-drag-conv]');
23
+ if (!target || !draggedId || target.dataset.dragConv === draggedId) return;
24
+ const pinnedItems = [...this.listEl.querySelectorAll('[data-drag-conv]')];
25
+ const draggedEl = pinnedItems.find(el => el.dataset.dragConv === draggedId);
26
+ if (!draggedEl) return;
27
+ this.listEl.insertBefore(draggedEl, target);
28
+ const newOrder = [...this.listEl.querySelectorAll('[data-drag-conv]')].map(el => el.dataset.dragConv);
29
+ window.wsClient?.rpc('conv.reorder', { order: newOrder }).catch(() => {});
30
+ });
31
+ const bulkBar = document.getElementById('bulkActionBar');
32
+ const bulkCount = document.getElementById('bulkCount');
33
+ const updateBulkBar = () => {
34
+ const checked = this.getSelectedIds().length;
35
+ if (bulkBar) bulkBar.style.display = checked > 0 ? 'flex' : 'none';
36
+ if (bulkCount) bulkCount.textContent = `${checked} selected`;
37
+ };
38
+ this.listEl.addEventListener('change', (e) => {
39
+ if (e.target.matches('.conversation-item-checkbox')) updateBulkBar();
40
+ });
41
+ document.getElementById('bulkArchiveBtn')?.addEventListener('click', () => this.bulkArchive());
42
+ document.getElementById('bulkDeleteBtn')?.addEventListener('click', () => this.bulkDelete());
43
+ this.listEl.addEventListener('click', (e) => {
44
+ const exportBtn = e.target.closest('[data-export-conv]');
45
+ if (exportBtn) { e.stopPropagation(); this.exportConversation(exportBtn.dataset.exportConv); return; }
46
+ const archiveBtn = e.target.closest('[data-archive-conv]');
47
+ if (archiveBtn) { e.stopPropagation(); this.archiveConversation(archiveBtn.dataset.archiveConv); return; }
48
+ const deleteBtn = e.target.closest('[data-delete-conv]');
49
+ if (deleteBtn) {
50
+ e.stopPropagation();
51
+ const convId = deleteBtn.dataset.deleteConv;
52
+ const conv = this.conversations.find(c => c.id === convId);
53
+ this.confirmDelete(convId, conv?.title || 'Untitled');
54
+ return;
55
+ }
56
+ const item = e.target.closest('[data-conv-id]');
57
+ if (item) this.select(item.dataset.convId);
58
+ });
59
+ },
60
+
61
+ setupFolderBrowser() {
62
+ this.folderBrowser.modal = document.getElementById('folderBrowserModal');
63
+ this.folderBrowser.listEl = document.getElementById('folderList');
64
+ this.folderBrowser.breadcrumbEl = document.getElementById('folderBreadcrumb');
65
+ if (!this.folderBrowser.modal) return;
66
+ const closeBtn = this.folderBrowser.modal.querySelector('[data-folder-close]');
67
+ const cancelBtn = this.folderBrowser.modal.querySelector('[data-folder-cancel]');
68
+ const selectBtn = this.folderBrowser.modal.querySelector('[data-folder-select]');
69
+ closeBtn?.addEventListener('click', () => this.closeFolderBrowser());
70
+ cancelBtn?.addEventListener('click', () => this.closeFolderBrowser());
71
+ selectBtn?.addEventListener('click', () => this.confirmFolderSelection());
72
+ this.folderBrowser.modal.addEventListener('click', (e) => {
73
+ if (e.target === this.folderBrowser.modal) this.closeFolderBrowser();
74
+ });
75
+ this.folderBrowser.homePathReady = this.fetchHomePath();
76
+ },
77
+
78
+ async fetchHomePath() {
79
+ try {
80
+ const res = await fetch(`${window.__BASE_URL || '/gm'}/api/home`);
81
+ const data = await res.json();
82
+ this.folderBrowser.homePath = data.home || '~';
83
+ this.folderBrowser.cwdPath = data.cwd || null;
84
+ } catch (e) { console.error('Failed to fetch home path:', e); }
85
+ },
86
+
87
+ async openFolderBrowser() {
88
+ window.dispatchEvent(new CustomEvent('preparing-new-conversation'));
89
+ if (!this.folderBrowser.modal) { this.createNew(); return; }
90
+ if (this.folderBrowser.homePathReady) await this.folderBrowser.homePathReady;
91
+ const startPath = this.folderBrowser.cwdPath || '~';
92
+ this.folderBrowser.currentPath = startPath;
93
+ this.folderBrowser.modal.classList.add('visible');
94
+ this.loadFolders(startPath);
95
+ },
96
+
97
+ closeFolderBrowser() { this.folderBrowser.modal?.classList.remove('visible'); },
98
+
99
+ async loadFolders(dirPath) {
100
+ this.folderBrowser.currentPath = dirPath;
101
+ this.renderBreadcrumb(dirPath);
102
+ if (!this.folderBrowser.listEl) return;
103
+ this.folderBrowser.listEl.innerHTML = '<li class="folder-list-loading">Loading...</li>';
104
+ try {
105
+ const data = await window.wsClient.rpc('folders', { path: dirPath });
106
+ const folders = data.folders || [];
107
+ this.folderBrowser.listEl.innerHTML = '';
108
+ const isAtRoot = dirPath === '~' || dirPath === '/' || dirPath === this.folderBrowser.homePath || /^[A-Za-z]:[\/\\]?$/.test(dirPath);
109
+ if (!isAtRoot) {
110
+ const parentPath = this.getParentPath(dirPath);
111
+ const upItem = document.createElement('li');
112
+ upItem.className = 'folder-list-item';
113
+ upItem.innerHTML = '<span class="folder-list-item-icon">..</span><span class="folder-list-item-name">Parent Directory</span>';
114
+ upItem.addEventListener('click', () => this.loadFolders(parentPath));
115
+ this.folderBrowser.listEl.appendChild(upItem);
116
+ }
117
+ if (folders.length === 0 && this.folderBrowser.listEl.children.length === 0) {
118
+ this.folderBrowser.listEl.innerHTML = '<li class="folder-list-empty">No subdirectories</li>';
119
+ return;
120
+ }
121
+ for (const folder of folders) {
122
+ const li = document.createElement('li');
123
+ li.className = 'folder-list-item';
124
+ li.innerHTML = `<span class="folder-list-item-icon">&#128193;</span><span class="folder-list-item-name">${this.escapeHtml(folder.name)}</span>`;
125
+ li.addEventListener('click', () => {
126
+ const expandedBase = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
127
+ const separator = expandedBase.includes('\\') ? '\\' : '/';
128
+ const base = expandedBase.replace(/[\/\\]+$/, '');
129
+ this.loadFolders(base + separator + folder.name);
130
+ });
131
+ this.folderBrowser.listEl.appendChild(li);
132
+ }
133
+ } catch (err) {
134
+ this.folderBrowser.listEl.innerHTML = `<li class="folder-list-error">Error: ${this.escapeHtml(err.message)}</li>`;
135
+ }
136
+ },
137
+
138
+ getParentPath(dirPath) {
139
+ const expanded = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
140
+ const parts = pathSplit(expanded);
141
+ const isWindows = expanded.includes('\\') || /^[A-Za-z]:/.test(expanded);
142
+ const separator = isWindows ? '\\' : '/';
143
+ if (parts.length <= 1) return isWindows && parts[0] && parts[0].endsWith(':') ? parts[0] + separator : separator;
144
+ parts.pop();
145
+ if (isWindows) { const joined = parts.join(separator); return parts.length === 1 && parts[0].endsWith(':') ? joined + separator : joined; }
146
+ return separator + parts.join(separator);
147
+ },
148
+
149
+ renderBreadcrumb(dirPath) {
150
+ if (!this.folderBrowser.breadcrumbEl) return;
151
+ const expanded = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
152
+ const parts = pathSplit(expanded);
153
+ const isWin = expanded.includes('\\') || /^[A-Za-z]:/.test(expanded);
154
+ const separator = isWin ? '\\' : '/';
155
+ const rootPath = isWin && parts[0] && /^[A-Za-z]:$/.test(parts[0]) ? parts[0] + separator : separator;
156
+ let html = `<span class="folder-breadcrumb-segment" data-path="${this.escapeHtml(rootPath)}">${this.escapeHtml(rootPath)} </span>`;
157
+ let accumulated = '';
158
+ const isDriveLetter = isWin && parts[0] && /^[A-Za-z]:$/.test(parts[0]);
159
+ for (let i = 0; i < parts.length; i++) {
160
+ if (i === 0 && isDriveLetter) accumulated = parts[0];
161
+ else accumulated += separator + parts[i];
162
+ const segPath = (i === 0 && isDriveLetter) ? rootPath : accumulated;
163
+ html += `<span class="folder-breadcrumb-separator">${separator}</span>`;
164
+ html += `<span class="folder-breadcrumb-segment" data-path="${this.escapeHtml(segPath)}">${this.escapeHtml(parts[i])}</span>`;
165
+ }
166
+ this.folderBrowser.breadcrumbEl.innerHTML = html;
167
+ this.folderBrowser.breadcrumbEl.querySelectorAll('.folder-breadcrumb-segment').forEach(seg => {
168
+ seg.addEventListener('click', () => { const p = seg.dataset.path; if (p) this.loadFolders(p); });
169
+ });
170
+ },
171
+
172
+ confirmFolderSelection() {
173
+ const currentPath = this.folderBrowser.currentPath;
174
+ const expanded = currentPath === '~' ? this.folderBrowser.homePath : currentPath;
175
+ this.closeFolderBrowser();
176
+ window.dispatchEvent(new CustomEvent('create-new-conversation', { detail: { workingDirectory: expanded, title: pathBasename(expanded) || 'root' } }));
177
+ },
178
+
179
+ showLoading() {
180
+ if (!this.listEl) return;
181
+ this.listEl.innerHTML = '';
182
+ if (this.emptyEl) { this.emptyEl.textContent = 'Loading...'; this.emptyEl.style.display = 'block'; }
183
+ }
184
+ });