@adhdev/daemon-core 0.5.36 → 0.5.38
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/dist/index.d.ts +5 -0
- package/dist/index.js +66 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/providers/_builtin/cli/claude-cli/provider.json +1 -1
- package/providers/_builtin/extension/codex/scripts/1.0/explore_dom.js +8 -2
- package/providers/_builtin/extension/codex/scripts/1.0/list_modes.js +138 -0
- package/providers/_builtin/extension/codex/scripts/1.0/read_chat.js +183 -34
- package/providers/_builtin/extension/codex/scripts/1.0/resolve_action.js +55 -15
- package/providers/_builtin/extension/codex/scripts/1.0/send_message.js +5 -25
- package/providers/_builtin/extension/codex/scripts/1.0/set_mode.js +165 -0
- package/src/agent-stream/provider-adapter.ts +1 -0
- package/src/agent-stream/types.ts +1 -0
- package/src/cdp/manager.ts +7 -1
- package/src/config/config.ts +19 -0
- package/src/daemon/dev-server.ts +5 -1
- package/src/providers/extension-provider-instance.ts +9 -1
- package/src/providers/ide-provider-instance.ts +4 -3
- package/src/providers/provider-loader.ts +26 -2
- package/src/status/reporter.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.38",
|
|
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",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsup",
|
|
15
|
-
"dev": "tsup --watch"
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"dist",
|
|
@@ -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
|
|
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" },
|
|
@@ -45,11 +45,17 @@
|
|
|
45
45
|
|
|
46
46
|
// 6. Check if we're on a task list or chat view
|
|
47
47
|
const hasConversationList = document.querySelectorAll('[role="button"][class*="rounded-lg"]').length > 0;
|
|
48
|
-
const
|
|
48
|
+
const headerEl = document.querySelector('[style*="view-transition-name: header-title"]');
|
|
49
|
+
const headerText = (headerEl?.textContent || '').trim();
|
|
50
|
+
const isTaskList = headerText === '작업' || headerText === 'Tasks';
|
|
51
|
+
|
|
52
|
+
if (isTaskList) {
|
|
53
|
+
return JSON.stringify({ __adhdev_skip_iframe: true, error: 'Found Tasks webview instead of Chat' });
|
|
54
|
+
}
|
|
49
55
|
|
|
50
56
|
return JSON.stringify({
|
|
51
57
|
headerText,
|
|
52
|
-
isTaskList
|
|
58
|
+
isTaskList,
|
|
53
59
|
modelFound: modelMatch ? modelMatch[0] : null,
|
|
54
60
|
textareaCount: textareas.length,
|
|
55
61
|
textareas: Array.from(textareas).map(t => ({
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
(() => {
|
|
8
|
+
try {
|
|
9
|
+
function resolveDoc() {
|
|
10
|
+
let doc = document;
|
|
11
|
+
let root = doc.getElementById('root');
|
|
12
|
+
if (!root) {
|
|
13
|
+
const iframes = doc.querySelectorAll('iframe');
|
|
14
|
+
for (const iframe of iframes) {
|
|
15
|
+
try {
|
|
16
|
+
const innerDoc = iframe.contentDocument || iframe.contentWindow?.document;
|
|
17
|
+
if (innerDoc?.getElementById('root')) {
|
|
18
|
+
doc = innerDoc;
|
|
19
|
+
root = innerDoc.getElementById('root');
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
} catch (e) { /* cross-origin */ }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { doc, root };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isModelMenuButton(b) {
|
|
29
|
+
const text = (b.textContent || '').trim();
|
|
30
|
+
if (b.getAttribute('aria-haspopup') !== 'menu') return false;
|
|
31
|
+
return /^(GPT-|gpt-|o\d|claude-|sonnet|opus)/i.test(text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Mode chip: menu trigger in composer that is not the model selector. */
|
|
35
|
+
function findModeMenuButton(doc) {
|
|
36
|
+
const composer =
|
|
37
|
+
doc.querySelector('[class*="thread-composer-max-width"]') ||
|
|
38
|
+
doc.querySelector('[class*="thread-composer"]') ||
|
|
39
|
+
doc.getElementById('root') ||
|
|
40
|
+
doc.body;
|
|
41
|
+
|
|
42
|
+
const buttons = Array.from(composer.querySelectorAll('button')).filter(
|
|
43
|
+
(b) => b.offsetWidth > 0 && b.offsetHeight > 0,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const menuTriggers = buttons.filter(
|
|
47
|
+
(b) => b.getAttribute('aria-haspopup') === 'menu' && !isModelMenuButton(b),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (menuTriggers.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
const byAria = menuTriggers.find((b) => {
|
|
53
|
+
const al = (b.getAttribute('aria-label') || '').toLowerCase();
|
|
54
|
+
return /mode|agent|ask|plan|autonomy|codex|모드|에이전트|플랜/i.test(al);
|
|
55
|
+
});
|
|
56
|
+
if (byAria) return byAria;
|
|
57
|
+
|
|
58
|
+
return menuTriggers[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function openMenu(btn) {
|
|
62
|
+
const rect = btn.getBoundingClientRect();
|
|
63
|
+
const cx = rect.left + rect.width / 2;
|
|
64
|
+
const cy = rect.top + rect.height / 2;
|
|
65
|
+
btn.dispatchEvent(
|
|
66
|
+
new PointerEvent('pointerdown', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
|
|
67
|
+
);
|
|
68
|
+
btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }));
|
|
69
|
+
btn.dispatchEvent(
|
|
70
|
+
new PointerEvent('pointerup', { bubbles: true, clientX: cx, clientY: cy, pointerId: 1 }),
|
|
71
|
+
);
|
|
72
|
+
btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: cx, clientY: cy }));
|
|
73
|
+
btn.dispatchEvent(new MouseEvent('click', { bubbles: true, clientX: cx, clientY: cy }));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { doc, root } = resolveDoc();
|
|
77
|
+
if (!root) return JSON.stringify({ modes: [], current: '', currentMode: '', error: 'no root' });
|
|
78
|
+
|
|
79
|
+
const modeBtn = findModeMenuButton(doc);
|
|
80
|
+
if (!modeBtn) {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
modes: [],
|
|
83
|
+
current: '',
|
|
84
|
+
currentMode: '',
|
|
85
|
+
error: 'mode menu button not found',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const currentLabel = (modeBtn.textContent || '').trim();
|
|
90
|
+
openMenu(modeBtn);
|
|
91
|
+
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
let menu = doc.querySelector('[role="menu"][data-state="open"]');
|
|
95
|
+
if (!menu) {
|
|
96
|
+
menu = doc.querySelector('[role="menu"]');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const collected = [];
|
|
100
|
+
if (menu) {
|
|
101
|
+
const items = menu.querySelectorAll(
|
|
102
|
+
'[role="menuitem"], [role="menuitemradio"], [role="option"], div[class*="cursor-interaction"]',
|
|
103
|
+
);
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const text = (item.textContent || '').trim();
|
|
106
|
+
if (
|
|
107
|
+
text &&
|
|
108
|
+
text.length > 0 &&
|
|
109
|
+
text.length < 80 &&
|
|
110
|
+
!/^모델|^model\b|^select\b/i.test(text)
|
|
111
|
+
) {
|
|
112
|
+
collected.push(text);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
doc.dispatchEvent(
|
|
118
|
+
new KeyboardEvent('keydown', {
|
|
119
|
+
key: 'Escape',
|
|
120
|
+
code: 'Escape',
|
|
121
|
+
keyCode: 27,
|
|
122
|
+
bubbles: true,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const modes = [...new Set(collected)];
|
|
127
|
+
const out = {
|
|
128
|
+
modes: modes.length > 0 ? modes : currentLabel ? [currentLabel] : [],
|
|
129
|
+
current: currentLabel,
|
|
130
|
+
currentMode: currentLabel,
|
|
131
|
+
};
|
|
132
|
+
resolve(JSON.stringify(out));
|
|
133
|
+
}, 550);
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return JSON.stringify({ error: e.message || String(e), modes: [], current: '', currentMode: '' });
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
@@ -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
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
//
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
*
|
|
7
|
+
* Placeholders: ${action}, ${button}
|
|
8
8
|
*/
|
|
9
9
|
(() => {
|
|
10
10
|
try {
|
|
11
|
-
const action = ${
|
|
11
|
+
const action = ${action};
|
|
12
|
+
const buttonText = ${button};
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
37
|
-
available: buttons.map(b => (b
|
|
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) });
|
|
@@ -12,35 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
// Find ProseMirror editor
|
|
14
14
|
const editor = document.querySelector('.ProseMirror');
|
|
15
|
-
if (!editor) return JSON.stringify({ error: '
|
|
15
|
+
if (!editor) return JSON.stringify({ error: 'Editor not found' });
|
|
16
16
|
|
|
17
17
|
// Focus the editor
|
|
18
18
|
editor.focus();
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Dispatch input event for ProseMirror to detect the change
|
|
25
|
-
editor.dispatchEvent(new InputEvent('input', {
|
|
26
|
-
bubbles: true,
|
|
27
|
-
cancelable: true,
|
|
28
|
-
inputType: 'insertText',
|
|
29
|
-
data: message,
|
|
30
|
-
}));
|
|
31
|
-
} else {
|
|
32
|
-
// Fallback: create new paragraph
|
|
33
|
-
const p = document.createElement('p');
|
|
34
|
-
p.textContent = message;
|
|
35
|
-
editor.innerHTML = '';
|
|
36
|
-
editor.appendChild(p);
|
|
37
|
-
editor.dispatchEvent(new InputEvent('input', {
|
|
38
|
-
bubbles: true,
|
|
39
|
-
cancelable: true,
|
|
40
|
-
inputType: 'insertText',
|
|
41
|
-
data: message,
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
20
|
+
// Use execCommand to safely insert text. This avoids TrustedHTML errors
|
|
21
|
+
// and naturally triggers ProseMirror's state updates and Keyboard/Input events.
|
|
22
|
+
document.execCommand('selectAll', false, null);
|
|
23
|
+
document.execCommand('insertText', false, message);
|
|
44
24
|
|
|
45
25
|
// Wait a tick then submit via Enter key
|
|
46
26
|
setTimeout(() => {
|