agentgui 1.0.700 → 1.0.702

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.
@@ -9,6 +9,7 @@ export function register(router, deps) {
9
9
  const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
10
10
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
11
11
  startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
12
+ startCodexDeviceAuth, codexDeviceAuthState,
12
13
  STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents } = deps;
13
14
 
14
15
  router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
@@ -157,6 +158,30 @@ export function register(router, deps) {
157
158
  } catch (e) { err(400, e.message); }
158
159
  });
159
160
 
161
+ router.handle('codex.start', async () => {
162
+ try {
163
+ const result = startCodexDeviceAuth();
164
+ const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
165
+ if (result.alreadyAuthenticated) return { status: 'success', authenticated: true };
166
+ const waitForCode = () => new Promise((resolve) => {
167
+ let tries = 12;
168
+ const poll = () => {
169
+ const state = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
170
+ if (state.authUrl || tries-- <= 0) resolve(state);
171
+ else setTimeout(poll, 400);
172
+ };
173
+ poll();
174
+ });
175
+ const state = await waitForCode();
176
+ return { status: state.status, authUrl: state.authUrl, userCode: state.userCode };
177
+ } catch (e) { err(500, e.message); }
178
+ });
179
+
180
+ router.handle('codex.status', () => {
181
+ const st = typeof codexDeviceAuthState === 'function' ? codexDeviceAuthState() : codexDeviceAuthState;
182
+ return st;
183
+ });
184
+
160
185
  router.handle('gemini.complete', async (p) => {
161
186
  const pastedUrl = (p.url || '').trim();
162
187
  if (!pastedUrl) err(400, 'No URL provided');
@@ -268,47 +293,47 @@ export function register(router, deps) {
268
293
  }
269
294
  });
270
295
 
271
- router.handle('agent.subagents', async (p) => {
272
- const { id } = p;
273
- if (!id) err(400, 'Missing agent id');
274
-
275
- // Claude Code: run 'claude agents list' and parse output
276
- if (id === 'claude-code' || id === 'cli-claude') {
277
- const spawnEnv = { ...process.env };
278
- delete spawnEnv.CLAUDECODE;
279
- const result = spawnSync('claude', ['agents', 'list'], {
280
- encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
281
- env: spawnEnv
282
- });
283
- if (result.status !== 0 || !result.stdout) return { subAgents: [] };
284
- const output = result.stdout.trim();
285
- // Output format: ' agentId · model' lines under section headers
286
- const agents = [];
287
- for (const line of output.split('\n').filter(l => l.trim())) {
288
- const match = line.match(/^ (\S+)\s+·/);
289
- if (match) {
290
- const id = match[1];
291
- agents.push({ id, name: id });
292
- }
293
- }
294
- console.log('[agent.subagents] claude agents list found:', agents.map(a => a.id).join(', '));
295
- return { subAgents: agents };
296
- }
297
-
298
- // ACP agents: hardcoded map filtered by installed tools
299
- const subAgentMap = {
300
- 'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
301
- 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
302
- 'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
303
- 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
304
- 'kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
305
- 'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
306
- 'codex': [],
307
- 'cli-codex': []
308
- };
309
- const subAgents = subAgentMap[id] || [];
310
- const tools = await toolManager.getAllToolsAsync();
311
- const installed = new Set(tools.filter(t => t.category === 'plugin' && t.installed).map(t => t.id));
312
- return { subAgents: subAgents.filter(sa => installed.has(sa.id)) };
296
+ router.handle('agent.subagents', async (p) => {
297
+ const { id } = p;
298
+ if (!id) err(400, 'Missing agent id');
299
+
300
+ // Claude Code: run 'claude agents list' and parse output
301
+ if (id === 'claude-code' || id === 'cli-claude') {
302
+ const spawnEnv = { ...process.env };
303
+ delete spawnEnv.CLAUDECODE;
304
+ const result = spawnSync('claude', ['agents', 'list'], {
305
+ encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
306
+ env: spawnEnv
307
+ });
308
+ if (result.status !== 0 || !result.stdout) return { subAgents: [] };
309
+ const output = result.stdout.trim();
310
+ // Output format: ' agentId · model' lines under section headers
311
+ const agents = [];
312
+ for (const line of output.split('\n').filter(l => l.trim())) {
313
+ const match = line.match(/^ (\S+)\s+·/);
314
+ if (match) {
315
+ const id = match[1];
316
+ agents.push({ id, name: id });
317
+ }
318
+ }
319
+ console.log('[agent.subagents] claude agents list found:', agents.map(a => a.id).join(', '));
320
+ return { subAgents: agents };
321
+ }
322
+
323
+ // ACP agents: hardcoded map filtered by installed tools
324
+ const subAgentMap = {
325
+ 'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
326
+ 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
327
+ 'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
328
+ 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
329
+ 'kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
330
+ 'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
331
+ 'codex': [],
332
+ 'cli-codex': []
333
+ };
334
+ const subAgents = subAgentMap[id] || [];
335
+ const tools = await toolManager.getAllToolsAsync();
336
+ const installed = new Set(tools.filter(t => t.category === 'plugin' && t.installed).map(t => t.id));
337
+ return { subAgents: subAgents.filter(sa => installed.has(sa.id)) };
313
338
  });
314
339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.700",
3
+ "version": "1.0.702",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -696,6 +696,96 @@ const GEMINI_ACCOUNTS_FILE = path.join(GEMINI_DIR, 'google_accounts.json');
696
696
  let geminiOAuthState = { status: 'idle', error: null, email: null };
697
697
  let geminiOAuthPending = null;
698
698
 
699
+ const CODEX_AUTH_FILE = path.join(os.homedir(), '.codex', 'auth.json');
700
+ let codexDeviceAuthState = { status: 'idle', error: null, userCode: null, authUrl: null };
701
+ let codexDeviceAuthProcess = null;
702
+
703
+ function getCodexAuthStatus() {
704
+ try {
705
+ if (fs.existsSync(CODEX_AUTH_FILE)) {
706
+ const creds = JSON.parse(fs.readFileSync(CODEX_AUTH_FILE, 'utf-8'));
707
+ if (creds.auth_mode === 'oauth' || creds.access_token || creds.refresh_token) {
708
+ return { authenticated: true, detail: creds.email || 'oauth' };
709
+ }
710
+ if (creds.auth_mode === 'apikey' && creds.OPENAI_API_KEY) {
711
+ return { authenticated: true, detail: 'api-key' };
712
+ }
713
+ }
714
+ } catch (_) {}
715
+ return { authenticated: false, detail: 'no credentials' };
716
+ }
717
+
718
+ function startCodexDeviceAuth() {
719
+ if (codexDeviceAuthProcess) {
720
+ return { alreadyRunning: true };
721
+ }
722
+ const codexStatus = getCodexAuthStatus();
723
+ if (codexStatus.authenticated) {
724
+ codexDeviceAuthState = { status: 'success', error: null, userCode: null, authUrl: null };
725
+ return { alreadyAuthenticated: true };
726
+ }
727
+ codexDeviceAuthState = { status: 'pending', error: null, userCode: null, authUrl: null };
728
+ const child = spawn('npx', ['@openai/codex', 'login', '--device-auth'], {
729
+ stdio: ['ignore', 'pipe', 'pipe'],
730
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
731
+ shell: os.platform() === 'win32',
732
+ });
733
+ codexDeviceAuthProcess = child;
734
+
735
+ const onData = (chunk) => {
736
+ const text = chunk.toString().replace(/\x1b\[[0-9;]*m/g, '');
737
+ const urlMatch = text.match(/https:\/\/auth\.openai\.com\/codex\/device[^\s]*/);
738
+ const codeMatch = text.match(/\b([0-9A-Z]{4}-[0-9A-Z]{5})\b/);
739
+ if (urlMatch && !codexDeviceAuthState.authUrl) codexDeviceAuthState.authUrl = urlMatch[0];
740
+ if (codeMatch && !codexDeviceAuthState.userCode) codexDeviceAuthState.userCode = codeMatch[1];
741
+ };
742
+ child.stdout.on('data', onData);
743
+ child.stderr.on('data', onData);
744
+
745
+ const authFileWatcher = fs.watchFile ? null : null;
746
+ const pollInterval = setInterval(() => {
747
+ const st = getCodexAuthStatus();
748
+ if (st.authenticated) {
749
+ clearInterval(pollInterval);
750
+ clearTimeout(timeoutId);
751
+ codexDeviceAuthState = { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl };
752
+ if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
753
+ }
754
+ }, 1500);
755
+
756
+ const timeoutId = setTimeout(() => {
757
+ clearInterval(pollInterval);
758
+ if (codexDeviceAuthState.status === 'pending') {
759
+ codexDeviceAuthState = { status: 'error', error: 'Authentication timed out', userCode: null, authUrl: null };
760
+ }
761
+ if (codexDeviceAuthProcess) { try { codexDeviceAuthProcess.kill('SIGTERM'); } catch (_) {} codexDeviceAuthProcess = null; }
762
+ }, 5 * 60 * 1000);
763
+
764
+ child.on('error', (e) => {
765
+ clearInterval(pollInterval);
766
+ clearTimeout(timeoutId);
767
+ codexDeviceAuthState = { status: 'error', error: e.message, userCode: null, authUrl: null };
768
+ codexDeviceAuthProcess = null;
769
+ });
770
+ child.on('close', (code) => {
771
+ clearInterval(pollInterval);
772
+ clearTimeout(timeoutId);
773
+ codexDeviceAuthProcess = null;
774
+ if (codexDeviceAuthState.status === 'pending') {
775
+ if (code === 0) {
776
+ const st = getCodexAuthStatus();
777
+ codexDeviceAuthState = st.authenticated
778
+ ? { status: 'success', error: null, userCode: codexDeviceAuthState.userCode, authUrl: codexDeviceAuthState.authUrl }
779
+ : { status: 'error', error: 'Process exited without saving credentials', userCode: null, authUrl: null };
780
+ } else {
781
+ codexDeviceAuthState = { status: 'error', error: 'Authentication cancelled', userCode: null, authUrl: null };
782
+ }
783
+ }
784
+ });
785
+
786
+ return { started: true };
787
+ }
788
+
699
789
  function buildBaseUrl(req) {
700
790
  const override = process.env.AGENTGUI_BASE_URL;
701
791
  if (override) return override.replace(/\/+$/, '');
@@ -2177,6 +2267,10 @@ const server = http.createServer(async (req, res) => {
2177
2267
  } else {
2178
2268
  status.detail = 'no credentials';
2179
2269
  }
2270
+ } else if (agent.id === 'codex') {
2271
+ const codexSt = getCodexAuthStatus();
2272
+ status.authenticated = codexSt.authenticated;
2273
+ status.detail = codexSt.detail;
2180
2274
  } else {
2181
2275
  status.detail = 'unknown';
2182
2276
  }
@@ -2659,6 +2753,55 @@ const server = http.createServer(async (req, res) => {
2659
2753
  }
2660
2754
  }
2661
2755
 
2756
+ if (agentId === 'codex') {
2757
+ const conversationId = '__agent_auth__';
2758
+ const result = startCodexDeviceAuth();
2759
+ if (result.alreadyRunning) {
2760
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: 'pending' });
2761
+ return;
2762
+ }
2763
+ if (result.alreadyAuthenticated) {
2764
+ sendJSON(req, res, 200, { ok: true, agentId, status: 'success' });
2765
+ return;
2766
+ }
2767
+ broadcastSync({ type: 'script_started', conversationId, script: 'auth-codex', agentId: 'codex', timestamp: Date.now() });
2768
+ broadcastSync({ type: 'script_output', conversationId, data: '\x1b[36mStarting Codex device authorization...\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
2769
+
2770
+ const waitForCode = (tries) => {
2771
+ if (tries <= 0) return;
2772
+ if (codexDeviceAuthState.authUrl && codexDeviceAuthState.userCode) {
2773
+ const msg = `\r\nVisit: ${codexDeviceAuthState.authUrl}\r\nEnter code: ${codexDeviceAuthState.userCode}\r\n`;
2774
+ broadcastSync({ type: 'script_output', conversationId, data: msg, stream: 'stdout', timestamp: Date.now() });
2775
+ return;
2776
+ }
2777
+ setTimeout(() => waitForCode(tries - 1), 500);
2778
+ };
2779
+ waitForCode(10);
2780
+
2781
+ const pollId = setInterval(() => {
2782
+ if (codexDeviceAuthState.status === 'success') {
2783
+ clearInterval(pollId);
2784
+ broadcastSync({ type: 'script_output', conversationId, data: '\r\n\x1b[32mCodex authentication successful\x1b[0m\r\n', stream: 'stdout', timestamp: Date.now() });
2785
+ broadcastSync({ type: 'script_stopped', conversationId, code: 0, timestamp: Date.now() });
2786
+ } else if (codexDeviceAuthState.status === 'error') {
2787
+ clearInterval(pollId);
2788
+ broadcastSync({ type: 'script_output', conversationId, data: `\r\n\x1b[31mAuthentication failed: ${codexDeviceAuthState.error}\x1b[0m\r\n`, stream: 'stderr', timestamp: Date.now() });
2789
+ broadcastSync({ type: 'script_stopped', conversationId, code: 1, error: codexDeviceAuthState.error, timestamp: Date.now() });
2790
+ }
2791
+ }, 1500);
2792
+ setTimeout(() => clearInterval(pollId), 5 * 60 * 1000 + 5000);
2793
+
2794
+ const waitForInfo = (tries) => {
2795
+ if (codexDeviceAuthState.authUrl || tries <= 0) {
2796
+ sendJSON(req, res, 200, { ok: true, agentId, authUrl: codexDeviceAuthState.authUrl, userCode: codexDeviceAuthState.userCode, status: codexDeviceAuthState.status });
2797
+ } else {
2798
+ setTimeout(() => waitForInfo(tries - 1), 400);
2799
+ }
2800
+ };
2801
+ waitForInfo(12);
2802
+ return;
2803
+ }
2804
+
2662
2805
  const authCommands = {
2663
2806
  'claude-code': { cmd: 'claude', args: ['setup-token'] },
2664
2807
  'opencode': { cmd: 'opencode', args: ['auth', 'login'] },
@@ -4190,6 +4333,7 @@ registerUtilHandlers(wsRouter, {
4190
4333
  broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
4191
4334
  startGeminiOAuth, exchangeGeminiOAuthCode,
4192
4335
  geminiOAuthState: () => geminiOAuthState,
4336
+ startCodexDeviceAuth, codexDeviceAuthState: () => codexDeviceAuthState,
4193
4337
  STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents
4194
4338
  });
4195
4339
 
@@ -128,6 +128,7 @@
128
128
  function closeDropdown() { dropdown.classList.remove('open'); editingProvider = null; }
129
129
 
130
130
  var oauthPollInterval = null, oauthPollTimeout = null, oauthFallbackTimer = null;
131
+ var codexPollInterval = null, codexPollTimeout = null;
131
132
 
132
133
  function cleanupOAuthPolling() {
133
134
  if (oauthPollInterval) { clearInterval(oauthPollInterval); oauthPollInterval = null; }
@@ -135,6 +136,96 @@
135
136
  if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
136
137
  }
137
138
 
139
+ function cleanupCodexPolling() {
140
+ if (codexPollInterval) { clearInterval(codexPollInterval); codexPollInterval = null; }
141
+ if (codexPollTimeout) { clearTimeout(codexPollTimeout); codexPollTimeout = null; }
142
+ }
143
+
144
+ function removeCodexModal() {
145
+ var el = document.getElementById('codexDeviceModal');
146
+ if (el) el.remove();
147
+ }
148
+
149
+ function showCodexDeviceModal(authUrl, userCode) {
150
+ removeCodexModal();
151
+ var overlay = document.createElement('div');
152
+ overlay.id = 'codexDeviceModal';
153
+ 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;';
154
+ var displayUrl = authUrl || 'https://auth.openai.com/codex/device';
155
+ var displayCode = userCode || '';
156
+ overlay.innerHTML = '<div style="background:var(--color-bg-secondary,#1f2937);border-radius:1rem;padding:2rem;max-width:30rem;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()">' +
157
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1.25rem;">' +
158
+ '<h2 style="font-size:1.125rem;font-weight:700;margin:0;">Codex Sign-In</h2>' +
159
+ '<button id="codexModalClose" 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>' +
160
+ '<div id="codexModalContent" style="text-align:center;">' +
161
+ '<div style="font-size:2rem;margin-bottom:1rem;animation:pulse 2s infinite;">&#9203;</div>' +
162
+ '<p style="font-size:0.85rem;color:var(--color-text-secondary,#d1d5db);margin:0 0 1rem;">Follow these steps to sign in with your OpenAI account:</p>' +
163
+ '<div style="margin-bottom:1rem;">' +
164
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">1. Open this link in your browser:</p>' +
165
+ '<div style="display:flex;gap:0.5rem;align-items:center;">' +
166
+ '<a href="' + esc(displayUrl) + '" target="_blank" style="flex:1;padding:0.5rem 0.75rem;background:var(--color-primary,#3b82f6);color:white;text-decoration:none;border-radius:0.375rem;font-size:0.8rem;font-weight:600;text-align:center;">Open Sign-In Page</a>' +
167
+ '</div></div>' +
168
+ '<div style="margin-bottom:1.25rem;">' +
169
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0 0 0.5rem;text-align:left;">2. Enter this one-time code:</p>' +
170
+ '<div style="display:flex;gap:0.5rem;align-items:center;">' +
171
+ '<div id="codexUserCode" style="flex:1;padding:0.625rem;background:var(--color-bg-primary,#111827);border:2px solid var(--color-primary,#3b82f6);border-radius:0.5rem;font-family:monospace;font-size:1.25rem;font-weight:700;letter-spacing:0.15em;text-align:center;">' + esc(displayCode) + '</div>' +
172
+ '<button id="codexCopyBtn" style="padding:0.5rem 0.75rem;background:var(--color-bg-primary,#374151);border:1px solid var(--color-border,#4b5563);border-radius:0.375rem;color:var(--color-text-primary,white);font-size:0.75rem;cursor:pointer;flex-shrink:0;">Copy</button>' +
173
+ '</div></div>' +
174
+ '<p style="font-size:0.75rem;color:var(--color-text-secondary,#6b7280);margin:0;">This dialog will close automatically when sign-in completes.</p>' +
175
+ '</div>' +
176
+ '<div id="codexModalSuccess" style="display:none;text-align:center;padding:1rem 0;">' +
177
+ '<div style="font-size:3rem;color:#10b981;margin-bottom:0.75rem;">&#10003;</div>' +
178
+ '<p style="font-weight:600;margin:0 0 0.25rem;">Authentication Successful</p>' +
179
+ '<p style="font-size:0.8rem;color:var(--color-text-secondary,#9ca3af);margin:0;">Codex CLI is now authenticated.</p>' +
180
+ '</div>' +
181
+ '<div style="margin-top:1.25rem;">' +
182
+ '<button id="codexModalCancel" style="width:100%;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></div>' +
183
+ '<style>@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}</style></div>';
184
+ document.body.appendChild(overlay);
185
+
186
+ var dismiss = function() { cleanupCodexPolling(); authRunning = false; removeCodexModal(); };
187
+ document.getElementById('codexModalClose').addEventListener('click', dismiss);
188
+ document.getElementById('codexModalCancel').addEventListener('click', dismiss);
189
+
190
+ var copyBtn = document.getElementById('codexCopyBtn');
191
+ if (copyBtn && displayCode) {
192
+ copyBtn.addEventListener('click', function(e) {
193
+ e.stopPropagation();
194
+ navigator.clipboard.writeText(displayCode).then(function() {
195
+ copyBtn.textContent = 'Copied!';
196
+ setTimeout(function() { copyBtn.textContent = 'Copy'; }, 2000);
197
+ }).catch(function() {});
198
+ });
199
+ }
200
+
201
+ cleanupCodexPolling();
202
+ codexPollInterval = setInterval(function() {
203
+ window.wsClient.rpc('codex.status')
204
+ .then(function(st) {
205
+ if (st.status === 'success') {
206
+ cleanupCodexPolling();
207
+ authRunning = false;
208
+ var content = document.getElementById('codexModalContent');
209
+ var success = document.getElementById('codexModalSuccess');
210
+ var cancel = document.getElementById('codexModalCancel');
211
+ if (content) content.style.display = 'none';
212
+ if (success) success.style.display = 'block';
213
+ if (cancel) cancel.textContent = 'Close';
214
+ setTimeout(function() { removeCodexModal(); refresh(); }, 2500);
215
+ } else if (st.status === 'error') {
216
+ cleanupCodexPolling();
217
+ authRunning = false;
218
+ removeCodexModal();
219
+ refresh();
220
+ }
221
+ }).catch(function() {});
222
+ }, 1500);
223
+ codexPollTimeout = setTimeout(function() {
224
+ cleanupCodexPolling();
225
+ if (authRunning) { authRunning = false; removeCodexModal(); }
226
+ }, 5 * 60 * 1000);
227
+ }
228
+
138
229
  function showOAuthWaitingModal() {
139
230
  removeOAuthModal();
140
231
  var overlay = document.createElement('div');
@@ -217,6 +308,19 @@
217
308
 
218
309
  function triggerAuth(agentId) {
219
310
  if (authRunning) return;
311
+ if (agentId === 'codex') {
312
+ authRunning = true;
313
+ window.wsClient.rpc('codex.start')
314
+ .then(function(data) {
315
+ if (data.status === 'success') {
316
+ authRunning = false;
317
+ refresh();
318
+ return;
319
+ }
320
+ showCodexDeviceModal(data.authUrl, data.userCode);
321
+ }).catch(function() { authRunning = false; });
322
+ return;
323
+ }
220
324
  window.wsClient.rpc('agent.auth', { id: agentId })
221
325
  .then(function(data) {
222
326
  if (data.ok) {
@@ -271,6 +375,8 @@
271
375
  authRunning = false;
272
376
  removeOAuthModal();
273
377
  cleanupOAuthPolling();
378
+ cleanupCodexPolling();
379
+ removeCodexModal();
274
380
  var term = getTerminal();
275
381
  var msg = data.error ? data.error : ('exited with code ' + (data.code || 0));
276
382
  if (term) term.writeln('\r\n\x1b[90m[auth ' + msg + ']\x1b[0m');