@adhdev/daemon-core 0.5.42 → 0.5.44

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": "@adhdev/daemon-core",
3
- "version": "0.5.42",
3
+ "version": "0.5.44",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -86,7 +86,7 @@
86
86
  { "source": "Always\\s*allow", "flags": "i" },
87
87
  { "source": "\\(y/n\\)", "flags": "i" },
88
88
  { "source": "\\[Y/n\\]", "flags": "i" },
89
- { "source": "Run\\s*this\\s*command", "flags": "i" },
89
+ { "source": "Run\\s+\\w+\\s+command", "flags": "i" },
90
90
  { "source": "Allow\\s*tool", "flags": "i" },
91
91
  { "source": "Yes,?\\s*don'?t\\s*ask", "flags": "i" },
92
92
  { "source": "Deny", "flags": "i" },
@@ -1,88 +1,58 @@
1
1
  /**
2
- * Codex ExtensionDeep DOM Explorer
3
- * Explores the chat area, message list, input field, model selector etc.
2
+ * Debug scriptenumerate all buttons in all frames
4
3
  */
5
4
  (() => {
6
5
  try {
7
- const root = document.getElementById('root');
8
- if (!root) return JSON.stringify({ error: 'no root' });
9
-
10
- // 1. Find chat input (textarea, contenteditable, etc.)
11
- const textareas = document.querySelectorAll('textarea');
12
- const contentEditables = document.querySelectorAll('[contenteditable="true"]');
13
- const chatInputCandidates = document.querySelectorAll('[data-placeholder], [placeholder*="ask"], [placeholder*="message"], [placeholder*="type"]');
14
-
15
- // 2. Find message-like containers
16
- const messageContainers = [];
17
- // Look for common message list patterns
18
- const candidates = [
19
- ...document.querySelectorAll('[class*="message"]'),
20
- ...document.querySelectorAll('[class*="chat"]'),
21
- ...document.querySelectorAll('[class*="conversation"]'),
22
- ...document.querySelectorAll('[class*="thread"]'),
23
- ...document.querySelectorAll('[role="log"]'),
24
- ...document.querySelectorAll('[role="list"]'),
25
- ];
26
- for (const el of candidates) {
27
- messageContainers.push({
28
- tag: el.tagName?.toLowerCase(),
29
- class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
30
- role: el.getAttribute('role'),
31
- childCount: el.children?.length || 0,
32
- text: (el.textContent || '').trim().substring(0, 200),
33
- });
6
+ const results = {};
7
+
8
+ // Frame 1: document (child frame / execution context)
9
+ const allBtns1 = Array.from(document.querySelectorAll('button'));
10
+ const visibleBtns1 = allBtns1.filter(b => b.offsetWidth > 0);
11
+ const menuBtns1 = visibleBtns1.filter(b => b.getAttribute('aria-haspopup') === 'menu');
12
+ results.childFrame = {
13
+ hasRoot: !!document.getElementById('root'),
14
+ totalBtns: allBtns1.length,
15
+ visibleBtns: visibleBtns1.length,
16
+ menuBtns: menuBtns1.length,
17
+ menuBtnTexts: menuBtns1.map(b => (b.textContent||'').trim().slice(0,40)),
18
+ sampleBtnTexts: visibleBtns1.slice(0,10).map(b => ({
19
+ t: (b.textContent||'').trim().slice(0,40),
20
+ hp: b.getAttribute('aria-haspopup'),
21
+ w: b.offsetWidth
22
+ }))
23
+ };
24
+
25
+ // Frame 2: inner iframe (if available)
26
+ const iframes = document.querySelectorAll('iframe');
27
+ results.iframeCount = iframes.length;
28
+ for (let i = 0; i < Math.min(iframes.length, 3); i++) {
29
+ try {
30
+ const innerDoc = iframes[i].contentDocument || iframes[i].contentWindow?.document;
31
+ if (innerDoc) {
32
+ const allBtns2 = Array.from(innerDoc.querySelectorAll('button'));
33
+ const visibleBtns2 = allBtns2.filter(b => b.offsetWidth > 0);
34
+ const menuBtns2 = visibleBtns2.filter(b => b.getAttribute('aria-haspopup') === 'menu');
35
+ results['innerFrame_' + i] = {
36
+ hasRoot: !!innerDoc.getElementById('root'),
37
+ url: iframes[i].src?.slice(0, 100),
38
+ totalBtns: allBtns2.length,
39
+ visibleBtns: visibleBtns2.length,
40
+ menuBtns: menuBtns2.length,
41
+ menuBtnTexts: menuBtns2.map(b => (b.textContent||'').trim().slice(0,40)),
42
+ sampleBtnTexts: visibleBtns2.slice(0,10).map(b => ({
43
+ t: (b.textContent||'').trim().slice(0,40),
44
+ hp: b.getAttribute('aria-haspopup'),
45
+ w: b.offsetWidth
46
+ }))
47
+ };
48
+ }
49
+ } catch(e) {
50
+ results['innerFrame_' + i] = { error: e.message };
51
+ }
34
52
  }
35
-
36
- // 3. Find model/mode selectors
37
- const modelCandidates = document.querySelectorAll('[class*="model"], [class*="gpt"], [aria-label*="model" i], [aria-label*="모델" i]');
38
-
39
- // 4. Enumerate all role="button" items (conversation list items)
40
- const roleButtons = document.querySelectorAll('[role="button"]');
41
-
42
- // 5. Look for "GPT" text or model name in the UI
43
- const allText = root.textContent || '';
44
- const modelMatch = allText.match(/(GPT-[\d.]+|gpt-[\w.-]+|o\d+-[\w]+|claude-[\w.-]+)/i);
45
-
46
- // 6. Check if we're on a task list or chat view
47
- const hasConversationList = document.querySelectorAll('[role="button"][class*="rounded-lg"]').length > 0;
48
- const headerText = document.querySelector('[style*="view-transition-name: header-title"]')?.textContent?.trim() || '';
49
-
50
- return JSON.stringify({
51
- headerText,
52
- isTaskList: headerText === '작업' || headerText === 'Tasks',
53
- modelFound: modelMatch ? modelMatch[0] : null,
54
- textareaCount: textareas.length,
55
- textareas: Array.from(textareas).map(t => ({
56
- class: t.className?.substring(0, 200),
57
- placeholder: t.placeholder?.substring(0, 200),
58
- rows: t.rows,
59
- value: (t.value || '').substring(0, 100),
60
- rect: (() => { const r = t.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height) }; })(),
61
- })),
62
- contentEditableCount: contentEditables.length,
63
- contentEditables: Array.from(contentEditables).slice(0, 5).map(el => ({
64
- tag: el.tagName?.toLowerCase(),
65
- class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
66
- text: (el.textContent || '').trim().substring(0, 100),
67
- placeholder: el.getAttribute('data-placeholder')?.substring(0, 100),
68
- })),
69
- chatInputCandidates: Array.from(chatInputCandidates).slice(0, 5).map(el => ({
70
- tag: el.tagName?.toLowerCase(),
71
- class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
72
- placeholder: el.getAttribute('placeholder') || el.getAttribute('data-placeholder'),
73
- })),
74
- messageContainers: messageContainers.slice(0, 15),
75
- modelCandidates: Array.from(modelCandidates).slice(0, 5).map(el => ({
76
- tag: el.tagName?.toLowerCase(),
77
- class: (el.className && typeof el.className === 'string') ? el.className.substring(0, 200) : null,
78
- text: (el.textContent || '').trim().substring(0, 100),
79
- })),
80
- conversationButtons: Array.from(roleButtons).slice(0, 5).map(b => ({
81
- class: (b.className && typeof b.className === 'string') ? b.className.substring(0, 200) : null,
82
- text: (b.textContent || '').trim().substring(0, 150),
83
- })),
84
- });
85
- } catch (e) {
86
- return JSON.stringify({ error: e.message || String(e) });
53
+
54
+ return JSON.stringify(results);
55
+ } catch(e) {
56
+ return JSON.stringify({error: e.message});
87
57
  }
88
58
  })()
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Codex Extension — list_modes
3
+ *
4
+ * Finds the mode / autonomy dropdown next to the model chip in the composer footer,
5
+ * opens it (Radix), reads options, closes. UI expects `modes` + `current` (see ModelModeBar).
6
+ *
7
+ * Uses the same multi-searchRoot strategy as readChat for reliable mode button discovery.
8
+ */
9
+ (() => {
10
+ try {
11
+ function resolveDoc() {
12
+ let doc = document;
13
+ let root = doc.getElementById('root');
14
+ if (!root) {
15
+ const iframes = doc.querySelectorAll('iframe');
16
+ for (const iframe of iframes) {
17
+ try {
18
+ const innerDoc = iframe.contentDocument || iframe.contentWindow?.document;
19
+ if (innerDoc?.getElementById('root')) {
20
+ doc = innerDoc;
21
+ root = innerDoc.getElementById('root');
22
+ break;
23
+ }
24
+ } catch (e) { /* cross-origin */ }
25
+ }
26
+ }
27
+ return { doc, root };
28
+ }
29
+
30
+ function isModelText(text) {
31
+ return /^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text);
32
+ }
33
+
34
+ /**
35
+ * Find the mode menu button using the same multi-root strategy as readChat.
36
+ * Searches multiple container scopes and both doc frames.
37
+ */
38
+ function findModeButton(doc) {
39
+ for (const d of [doc, document]) {
40
+ const searchRoots = [
41
+ d.querySelector('[class*="thread-composer-max-width"]'),
42
+ d.querySelector('[class*="thread-composer"]'),
43
+ d.querySelector('[class*="pb-2"]'),
44
+ d.body,
45
+ ].filter(Boolean);
46
+
47
+ for (const searchRoot of searchRoots) {
48
+ const menuBtns = Array.from(searchRoot.querySelectorAll('button[aria-haspopup="menu"]'))
49
+ .filter(b => b.offsetWidth > 0);
50
+ for (const btn of menuBtns) {
51
+ const text = (btn.textContent || '').trim();
52
+ // Skip model buttons — we want the mode button
53
+ if (!isModelText(text) && text.length > 0 && text.length < 30) {
54
+ return btn;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ function openMenu(btn) {
63
+ const rect = btn.getBoundingClientRect();
64
+ const cx = rect.left + rect.width / 2;
65
+ const cy = rect.top + rect.height / 2;
66
+ btn.dispatchEvent(
67
+ new PointerEvent('pointerdown', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
68
+ );
69
+ btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }));
70
+ btn.dispatchEvent(
71
+ new PointerEvent('pointerup', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
72
+ );
73
+ btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: cx, clientY: cy }));
74
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: cx, clientY: cy }));
75
+ }
76
+
77
+ const { doc, root } = resolveDoc();
78
+ if (!root) return JSON.stringify({ modes: [], current: '', currentMode: '', error: 'no root' });
79
+
80
+ const modeBtn = findModeButton(doc);
81
+ if (!modeBtn) {
82
+ return JSON.stringify({
83
+ modes: [],
84
+ current: '',
85
+ currentMode: '',
86
+ error: 'mode menu button not found',
87
+ });
88
+ }
89
+
90
+ const menuDoc = modeBtn.ownerDocument || doc;
91
+ const currentLabel = (modeBtn.textContent || '').trim();
92
+ openMenu(modeBtn);
93
+
94
+ return new Promise((resolve) => {
95
+ setTimeout(() => {
96
+ let menu = menuDoc.querySelector('[role="menu"][data-state="open"]');
97
+ if (!menu) {
98
+ menu = menuDoc.querySelector('[role="menu"]');
99
+ }
100
+
101
+ const collected = [];
102
+ if (menu) {
103
+ const items = menu.querySelectorAll(
104
+ '[role="menuitem"], [role="menuitemradio"], [role="option"], div[class*="cursor-interaction"]',
105
+ );
106
+ for (const item of items) {
107
+ const text = (item.textContent || '').trim();
108
+ if (
109
+ text &&
110
+ text.length > 0 &&
111
+ text.length < 80 &&
112
+ !/^모델|^model\b|^select\b/i.test(text)
113
+ ) {
114
+ collected.push(text);
115
+ }
116
+ }
117
+ }
118
+
119
+ menuDoc.dispatchEvent(
120
+ new KeyboardEvent('keydown', {
121
+ key: 'Escape',
122
+ code: 'Escape',
123
+ keyCode: 27,
124
+ bubbles: true,
125
+ }),
126
+ );
127
+
128
+ const modes = [...new Set(collected)];
129
+ const out = {
130
+ modes: modes.length > 0 ? modes : currentLabel ? [currentLabel] : [],
131
+ current: currentLabel,
132
+ currentMode: currentLabel,
133
+ };
134
+ resolve(JSON.stringify(out));
135
+ }, 550);
136
+ });
137
+ } catch (e) {
138
+ return JSON.stringify({ error: e.message || String(e), modes: [], current: '', currentMode: '' });
139
+ }
140
+ })();
@@ -52,6 +52,11 @@
52
52
  const headerEl = doc.querySelector('[style*="view-transition-name: header-title"]');
53
53
  const headerText = (headerEl?.textContent || '').trim();
54
54
  const isTaskList = headerText === '작업' || headerText === 'Tasks';
55
+
56
+ // If we accidentally evaluated inside the Tasks webview instead of Chat, tell Daemon to try the next matching webview
57
+ if (isTaskList) {
58
+ return JSON.stringify({ __adhdev_skip_iframe: true, error: 'Found Tasks webview instead of Chat' });
59
+ }
55
60
 
56
61
  // ─── Rich content extractor ───
57
62
  const BLOCK_TAGS = new Set(['DIV', 'P', 'BR', 'LI', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'HR', 'SECTION', 'ARTICLE']);
@@ -291,24 +296,145 @@
291
296
 
292
297
  // ─── 3. Status ───
293
298
  let status = 'idle';
294
- const buttons = Array.from(doc.querySelectorAll('button'))
295
- .filter(b => b.offsetWidth > 0);
296
- const buttonTexts = buttons.map(b => (b.textContent || '').trim().toLowerCase());
297
- const buttonLabels = buttons.map(b => (b.getAttribute('aria-label') || '').toLowerCase());
298
-
299
- if (buttonTexts.includes('cancel') || buttonTexts.includes('취소') ||
300
- buttonLabels.some(l => l.includes('cancel') || l.includes('취소') || l.includes('stop') || l.includes('중지'))) {
299
+ // Filter out disabled buttons to avoid matching old historical prompts
300
+ // Include roles, inputs, custom vscode tags, and generic interactive lists used by multiple-choice forms
301
+ const selectors = [
302
+ 'button', '[role="radio"]', '[role="button"]', '[role="option"]', '[role="menuitem"]',
303
+ 'input[type="radio"] + label', 'input[type="radio"] ~ span', 'input[type="checkbox"] + label',
304
+ 'vscode-button', 'vscode-radio', 'vscode-checkbox', 'vscode-option', 'li'
305
+ ].join(', ');
306
+
307
+ const buttons = Array.from(doc.querySelectorAll(selectors))
308
+ .filter(b => b.offsetWidth > 0 && !b.disabled && !b.closest('[inert]'));
309
+
310
+ const getBtnLabel = (b) => {
311
+ let t = (b.textContent || '').trim();
312
+ return t || (b.getAttribute('aria-label') || '').trim();
313
+ };
314
+
315
+ const buttonLabels = buttons.map(getBtnLabel).map(t => t.toLowerCase());
316
+
317
+ if (buttonLabels.some(l => l.includes('cancel') || l.includes('취소') || l.includes('stop') || l.includes('중지'))) {
301
318
  status = 'generating';
302
319
  }
303
320
 
304
- // Codex approval buttons appear as distinct action buttons (e.g. "Approve", "Always approve", "Deny")
305
- // They typically appear at the bottom of a tool-call/action block
306
- const approvalSpecificPatterns = /^(approve|always approve|deny|reject|승인|항상 승인|거부)/i;
307
- const hasApprovalButton = buttonTexts.some(b => approvalSpecificPatterns.test(b)) ||
308
- buttonLabels.some(l => approvalSpecificPatterns.test(l));
309
- if (hasApprovalButton) {
310
- status = 'waiting_approval';
321
+ // ─── 5. Universal Approval Modal Detection (Language-Agnostic) ───
322
+ // The approval/interaction panel in Codex is rendered OUTSIDE the chat scroll area,
323
+ // typically in a sibling "request-input-panel" region at the bottom.
324
+ // We must scan the ENTIRE document body to find it.
325
+ let activeModal = null;
326
+
327
+ // Look for the request-input-panel area first (contains radio options + submit/skip)
328
+ // The approval panel lives in the outer webview document, NOT inside the inner iframe (doc).
329
+ // Use `document` (the webview frame root) to find it, same as explore_dom.js does.
330
+ const searchDocs = [document, doc]; // outer first, then inner iframe
331
+ let requestPanel = null;
332
+ for (const d of searchDocs) {
333
+ requestPanel = d.querySelector('[class*="request-input-panel"]');
334
+ if (requestPanel) break;
335
+ // Also search all textareas for the class
336
+ const tas = d.querySelectorAll('textarea');
337
+ for (const ta of tas) {
338
+ if (ta.className && ta.className.includes('request-input-panel')) {
339
+ requestPanel = ta;
340
+ break;
341
+ }
342
+ }
343
+ if (requestPanel) break;
311
344
  }
345
+ // Walk up from the request-input-panel to find the full approval card
346
+ let approvalArea = null;
347
+ if (requestPanel) {
348
+ let p = requestPanel;
349
+ for (let i = 0; i < 12 && p && p.parentElement; i++) {
350
+ p = p.parentElement;
351
+ const btns = p.querySelectorAll('button').length;
352
+ const radios = p.querySelectorAll('[role="radio"], [role="option"]').length;
353
+ const total = btns + radios;
354
+ if (btns >= 4) {
355
+ // Found the approval card with enough interactive elements
356
+ approvalArea = p;
357
+ break;
358
+ }
359
+ }
360
+ }
361
+
362
+
363
+
364
+
365
+ if (approvalArea) {
366
+ // ─── Codex Approval Panel (request-input-panel based) ───
367
+ // The approval form has:
368
+ // - Option items as plain divs (e.g., "1.\n예", "2.\n네,...", "3.\n아니요,...")
369
+ // - Action buttons (e.g., "건너뛰기", "제출⏎") as <button> elements
370
+ // We need to extract BOTH for the dashboard.
371
+
372
+ // Find parent container that has all options (level 3 from textarea, btns >= 4)
373
+ // approvalArea is already set to this level.
374
+
375
+ // Get the prompt message (from grandparent that starts with "Do you want...")
376
+ let messageText = '';
377
+ let msgParent = approvalArea.parentElement;
378
+ for (let i = 0; i < 5 && msgParent; i++) {
379
+ const t = (msgParent.innerText || '').trim();
380
+ if (t.length > 20 && /[??]/.test(t.substring(0, 200))) {
381
+ // Found a parent whose text starts with a question
382
+ messageText = t.split('\n')[0].trim();
383
+ break;
384
+ }
385
+ msgParent = msgParent.parentElement;
386
+ }
387
+ if (!messageText) {
388
+ // Fallback: use the approvalArea parent's text, first sentence
389
+ const parentText = (approvalArea.parentElement?.innerText || approvalArea.innerText || '').trim();
390
+ const firstLine = parentText.split('\n')[0].trim();
391
+ messageText = firstLine.length > 5 ? firstLine : 'Agent requires an interaction';
392
+ }
393
+
394
+ // Extract option labels from the panel's direct/nested children
395
+ const allBtns = Array.from(approvalArea.querySelectorAll('button'))
396
+ .filter(b => b.offsetWidth > 0 && !b.disabled)
397
+ .map(b => (b.textContent || '').trim())
398
+ .filter(t => t.length > 0 && t.length < 150);
399
+
400
+ // Extract numbered option text items (the entire panel text, split by option numbering)
401
+ const panelText = (approvalArea.innerText || '').trim();
402
+ // Parse "1.\n예\n2.\n네,...\n3.\n아니요,...\n건너뛰기\n제출⏎" format
403
+ const optionMatches = panelText.match(/\d+\.\n[^\n]+(?:\n[^\d][^\n]*)*/g) || [];
404
+ let options = optionMatches.map(o => o.replace(/\n/g, '').trim()).filter(o => o.length > 0);
405
+
406
+ // Clean: remove any trailing button labels that got captured in the last option
407
+ const btnLabelsSet = new Set(allBtns);
408
+ options = options.map(opt => {
409
+ let changed = true;
410
+ while (changed) {
411
+ changed = false;
412
+ for (const bl of btnLabelsSet) {
413
+ if (opt.endsWith(bl)) {
414
+ opt = opt.slice(0, -bl.length).trim();
415
+ changed = true;
416
+ }
417
+ }
418
+ }
419
+ return opt;
420
+ }).filter(o => o.length > 0);
421
+
422
+ // Merge numbered options from both sources (buttons + text parsing)
423
+ const allNumbered = [...new Set([...allBtns.filter(b => /^\d+\./.test(b)), ...options])];
424
+ allNumbered.sort((a, b) => (parseInt(a) || 999) - (parseInt(b) || 999));
425
+ const actionBtns = allBtns.filter(b => !/^\d+\./.test(b));
426
+
427
+ // Combine: sorted numbered options first, then action buttons (deduped)
428
+ const uniqueActions = [...new Set([...allNumbered, ...actionBtns])];
429
+
430
+ if (uniqueActions.length > 0) {
431
+ status = 'waiting_approval';
432
+ activeModal = {
433
+ message: messageText,
434
+ buttons: uniqueActions
435
+ };
436
+ }
437
+ } // end if (approvalArea)
312
438
 
313
439
  if (isTaskList) {
314
440
  status = messages.length === 0 ? 'idle' : status;
@@ -316,30 +442,53 @@
316
442
  if (!isVisible && messages.length === 0) status = 'panel_hidden';
317
443
 
318
444
  // ─── 4. Model / Mode ───
445
+ // Language-agnostic detection via DOM structure.
446
+ // Model: button text matching model name patterns (GPT-*, o1-*, claude-* — always English).
447
+ // Mode: non-model aria-haspopup="menu" button in composer area (language-agnostic).
319
448
  let model = '';
320
449
  let mode = '';
321
- const footerButtons = doc.querySelectorAll(
322
- '[class*="thread-composer-max-width"] button, [class*="pb-2"] button'
323
- );
324
- for (const btn of footerButtons) {
325
- const text = (btn.textContent || '').trim();
326
- if (/^(GPT-|gpt-|o\d|claude-)/i.test(text)) model = text;
327
- if (/^(낮음|중간|높음|low|medium|high)$/i.test(text)) mode = text;
328
- }
450
+ for (const d of [doc, document]) {
451
+ // Search in composer area, or common footer containers
452
+ const searchRoots = [
453
+ d.querySelector('[class*="thread-composer-max-width"]'),
454
+ d.querySelector('[class*="thread-composer"]'),
455
+ d.querySelector('[class*="pb-2"]'),
456
+ d.body,
457
+ ].filter(Boolean);
458
+
459
+ for (const searchRoot of searchRoots) {
460
+ if (model && mode) break;
461
+
462
+ // aria-haspopup="menu" buttons — dropdown triggers for model/mode
463
+ if (!model || !mode) {
464
+ const menuBtns = Array.from(searchRoot.querySelectorAll('button[aria-haspopup="menu"]'))
465
+ .filter(b => b.offsetWidth > 0);
466
+ for (const btn of menuBtns) {
467
+ const text = (btn.textContent || '').trim();
468
+ if (!model && /^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text)) {
469
+ model = text;
470
+ } else if (!mode && text.length > 0 && text.length < 30) {
471
+ mode = text;
472
+ }
473
+ }
474
+ }
329
475
 
330
- // ─── 5. Approval modal ───
331
- let activeModal = null;
332
- if (status === 'waiting_approval') {
333
- const approvalBtns = buttons
334
- .map(b => (b.textContent || '').trim())
335
- .filter(t => t && t.length > 0 && t.length < 40 &&
336
- /approve|accept|allow|confirm|run|proceed|cancel|deny|reject|승인|허용|실행|취소|거부/i.test(t));
337
- if (approvalBtns.length > 0) {
338
- activeModal = {
339
- message: 'Codex wants to perform an action',
340
- buttons: [...new Set(approvalBtns)],
341
- };
476
+ // Fallback: any visible button with model-like text
477
+ if (!model) {
478
+ const allBtns = Array.from(searchRoot.querySelectorAll('button'))
479
+ .filter(b => b.offsetWidth > 0);
480
+ for (const btn of allBtns) {
481
+ const text = (btn.textContent || '').trim();
482
+ if (/^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text)) {
483
+ model = text;
484
+ break;
485
+ }
486
+ }
487
+ }
488
+
489
+ if (model) break; // found model, no need to keep searching this scope
342
490
  }
491
+ if (model) break; // found in this frame
343
492
  }
344
493
 
345
494
  // ─── 6. Task info ───
@@ -2,39 +2,79 @@
2
2
  * Codex Extension — resolve_action
3
3
  *
4
4
  * Clicks approval/denial buttons in the Codex UI.
5
- * Actions: "approve", "deny", "cancel"
5
+ * Actions: "approve", "deny", "cancel", or raw button text
6
6
  *
7
- * Placeholder: ${ACTION}
7
+ * Placeholders: ${action}, ${button}
8
8
  */
9
9
  (() => {
10
10
  try {
11
- const action = ${ACTION};
11
+ const action = ${action};
12
+ const buttonText = ${button};
12
13
 
13
- const buttons = Array.from(document.querySelectorAll('button'))
14
- .filter(b => b.offsetWidth > 0);
14
+ // Search in document (outer webview frame where Codex approval panel lives)
15
+ const buttons = Array.from(document.querySelectorAll('button, [role="radio"], [role="button"], input[type="radio"] + label'))
16
+ .filter(b => b.offsetWidth > 0 && !b.disabled && !b.closest('[inert]'));
15
17
 
16
- // Map action to button text patterns
18
+ const actionLower = (action || '').toLowerCase();
17
19
  const patterns = {
18
20
  approve: /^(approve|accept|allow|confirm|run|proceed|yes|승인|허용|실행|확인)/i,
19
21
  deny: /^(deny|reject|no|거부|아니오)/i,
20
22
  cancel: /^(cancel|stop|취소|중지)/i,
21
23
  };
22
24
 
23
- const pattern = patterns[action.toLowerCase()] || patterns.approve;
25
+ // Determine what to search for: use buttonText if provided, otherwise match action pattern
26
+ const searchText = buttonText || action || '';
27
+ let targetBtn = null;
24
28
 
25
- for (const btn of buttons) {
26
- const text = (btn.textContent || '').trim();
27
- const label = btn.getAttribute('aria-label') || '';
28
- if (pattern.test(text) || pattern.test(label)) {
29
- btn.click();
30
- return JSON.stringify({ success: true, action, clicked: text || label });
29
+ const getBtnLabel = (b) => {
30
+ let t = (b.textContent || '').trim();
31
+ return t || (b.getAttribute('aria-label') || '').trim();
32
+ };
33
+
34
+ // 1. Try exact match on buttonText first
35
+ if (buttonText) {
36
+ targetBtn = buttons.find(b => getBtnLabel(b) === buttonText);
37
+ // 2. Try startsWith match (for cases where button text has extra chars like ⏎)
38
+ if (!targetBtn) {
39
+ targetBtn = buttons.find(b => getBtnLabel(b).startsWith(buttonText));
40
+ }
41
+ // 3. Try includes match
42
+ if (!targetBtn) {
43
+ targetBtn = buttons.find(b => getBtnLabel(b).includes(buttonText));
44
+ }
45
+ }
46
+
47
+ // 4. Fall back to pattern-based matching
48
+ if (!targetBtn && patterns[actionLower]) {
49
+ targetBtn = buttons.find(b => patterns[actionLower].test(getBtnLabel(b)));
50
+ }
51
+
52
+ // 5. Fall back to regex on raw action text
53
+ if (!targetBtn) {
54
+ const escapedAction = searchText.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
55
+ const pattern = new RegExp(escapedAction, 'i');
56
+ targetBtn = buttons.find(b => pattern.test(getBtnLabel(b)));
57
+ }
58
+
59
+ if (targetBtn) {
60
+ targetBtn.click();
61
+
62
+ // If this is a numbered option (1., 2., 3.), also click Submit after a short delay
63
+ const clickedText = getBtnLabel(targetBtn);
64
+ if (/^\d+\./.test(clickedText)) {
65
+ setTimeout(() => {
66
+ const submitBtn = buttons.find(b => /^(제출|submit)/i.test(getBtnLabel(b)));
67
+ if (submitBtn && submitBtn !== targetBtn) submitBtn.click();
68
+ }, 150);
31
69
  }
70
+
71
+ return JSON.stringify({ success: true, action, clicked: clickedText });
32
72
  }
33
73
 
34
74
  return JSON.stringify({
35
75
  success: false,
36
- error: `No button matching action '${action}' found`,
37
- available: buttons.map(b => (b.textContent || '').trim()).filter(t => t.length > 0 && t.length < 40),
76
+ error: `No button matching '${searchText}' found`,
77
+ available: buttons.map(b => getBtnLabel(b)).filter(t => t.length > 0 && t.length < 80),
38
78
  });
39
79
  } catch (e) {
40
80
  return JSON.stringify({ error: e.message || String(e) });