@adhdev/daemon-core 0.5.38 → 0.5.41
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.js +51 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/providers/_builtin/cli/claude-cli/provider.json +1 -1
- package/providers/_builtin/extension/codex/scripts/1.0/explore_dom.js +2 -8
- package/providers/_builtin/extension/codex/scripts/1.0/read_chat.js +34 -183
- package/providers/_builtin/extension/codex/scripts/1.0/resolve_action.js +15 -55
- package/providers/_builtin/extension/codex/scripts/1.0/send_message.js +25 -5
- package/src/commands/stream-commands.ts +37 -4
- package/src/daemon/dev-server.ts +20 -6
- package/src/status/reporter.ts +1 -1
- package/providers/_builtin/extension/codex/scripts/1.0/list_modes.js +0 -138
- package/providers/_builtin/extension/codex/scripts/1.0/set_mode.js +0 -165
package/package.json
CHANGED
|
@@ -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*this\\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,17 +45,11 @@
|
|
|
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
|
|
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
|
-
}
|
|
48
|
+
const headerText = document.querySelector('[style*="view-transition-name: header-title"]')?.textContent?.trim() || '';
|
|
55
49
|
|
|
56
50
|
return JSON.stringify({
|
|
57
51
|
headerText,
|
|
58
|
-
isTaskList,
|
|
52
|
+
isTaskList: headerText === '작업' || headerText === 'Tasks',
|
|
59
53
|
modelFound: modelMatch ? modelMatch[0] : null,
|
|
60
54
|
textareaCount: textareas.length,
|
|
61
55
|
textareas: Array.from(textareas).map(t => ({
|
|
@@ -52,11 +52,6 @@
|
|
|
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
|
-
}
|
|
60
55
|
|
|
61
56
|
// ─── Rich content extractor ───
|
|
62
57
|
const BLOCK_TAGS = new Set(['DIV', 'P', 'BR', 'LI', 'TR', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'HR', 'SECTION', 'ARTICLE']);
|
|
@@ -296,145 +291,24 @@
|
|
|
296
291
|
|
|
297
292
|
// ─── 3. Status ───
|
|
298
293
|
let status = 'idle';
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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('중지'))) {
|
|
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('중지'))) {
|
|
318
301
|
status = 'generating';
|
|
319
302
|
}
|
|
320
303
|
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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;
|
|
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';
|
|
344
311
|
}
|
|
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)
|
|
438
312
|
|
|
439
313
|
if (isTaskList) {
|
|
440
314
|
status = messages.length === 0 ? 'idle' : status;
|
|
@@ -442,53 +316,30 @@
|
|
|
442
316
|
if (!isVisible && messages.length === 0) status = 'panel_hidden';
|
|
443
317
|
|
|
444
318
|
// ─── 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).
|
|
448
319
|
let model = '';
|
|
449
320
|
let mode = '';
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
}
|
|
475
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
488
329
|
|
|
489
|
-
|
|
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
|
+
};
|
|
490
342
|
}
|
|
491
|
-
if (model) break; // found in this frame
|
|
492
343
|
}
|
|
493
344
|
|
|
494
345
|
// ─── 6. Task info ───
|
|
@@ -2,79 +2,39 @@
|
|
|
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"
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Placeholder: ${ACTION}
|
|
8
8
|
*/
|
|
9
9
|
(() => {
|
|
10
10
|
try {
|
|
11
|
-
const action = ${
|
|
12
|
-
const buttonText = ${button};
|
|
11
|
+
const action = ${ACTION};
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.filter(b => b.offsetWidth > 0 && !b.disabled && !b.closest('[inert]'));
|
|
13
|
+
const buttons = Array.from(document.querySelectorAll('button'))
|
|
14
|
+
.filter(b => b.offsetWidth > 0);
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
// Map action to button text patterns
|
|
19
17
|
const patterns = {
|
|
20
18
|
approve: /^(approve|accept|allow|confirm|run|proceed|yes|승인|허용|실행|확인)/i,
|
|
21
19
|
deny: /^(deny|reject|no|거부|아니오)/i,
|
|
22
20
|
cancel: /^(cancel|stop|취소|중지)/i,
|
|
23
21
|
};
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
const searchText = buttonText || action || '';
|
|
27
|
-
let targetBtn = null;
|
|
23
|
+
const pattern = patterns[action.toLowerCase()] || patterns.approve;
|
|
28
24
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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);
|
|
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 });
|
|
69
31
|
}
|
|
70
|
-
|
|
71
|
-
return JSON.stringify({ success: true, action, clicked: clickedText });
|
|
72
32
|
}
|
|
73
33
|
|
|
74
34
|
return JSON.stringify({
|
|
75
35
|
success: false,
|
|
76
|
-
error: `No button matching '${
|
|
77
|
-
available: buttons.map(b =>
|
|
36
|
+
error: `No button matching action '${action}' found`,
|
|
37
|
+
available: buttons.map(b => (b.textContent || '').trim()).filter(t => t.length > 0 && t.length < 40),
|
|
78
38
|
});
|
|
79
39
|
} catch (e) {
|
|
80
40
|
return JSON.stringify({ error: e.message || String(e) });
|
|
@@ -12,15 +12,35 @@
|
|
|
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: 'ProseMirror editor not found' });
|
|
16
16
|
|
|
17
17
|
// Focus the editor
|
|
18
18
|
editor.focus();
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// Clear existing content
|
|
21
|
+
const existingP = editor.querySelector('p');
|
|
22
|
+
if (existingP) {
|
|
23
|
+
existingP.textContent = message;
|
|
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
|
+
}
|
|
24
44
|
|
|
25
45
|
// Wait a tick then submit via Enter key
|
|
26
46
|
setTimeout(() => {
|
|
@@ -229,7 +229,15 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const scriptFn = provider.scripts[actualScriptName as keyof typeof provider.scripts] as Function;
|
|
232
|
-
|
|
232
|
+
// Normalize args: script placeholders use UPPERCASE (${MODE}, ${MODEL}, ${MESSAGE})
|
|
233
|
+
// but WebSocket args typically use lowercase. Add uppercase versions of common keys.
|
|
234
|
+
const normalizedArgs = { ...args };
|
|
235
|
+
for (const key of ['mode', 'model', 'message', 'action', 'button', 'text', 'sessionId']) {
|
|
236
|
+
if (key in normalizedArgs && !(key.toUpperCase() in normalizedArgs)) {
|
|
237
|
+
normalizedArgs[key.toUpperCase()] = normalizedArgs[key];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const scriptCode = scriptFn(normalizedArgs);
|
|
233
241
|
if (!scriptCode) return { success: false, error: `Script '${actualScriptName}' returned null` };
|
|
234
242
|
|
|
235
243
|
const cdpKey = provider.category === 'ide' ? (h.currentIdeType || agentType) : (h.currentIdeType || ideType);
|
|
@@ -249,10 +257,35 @@ export async function handleExtensionScript(h: CommandHelpers, args: any, script
|
|
|
249
257
|
break;
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
|
|
261
|
+
// IDE-level scripts (model/mode) — try session frame first, fallback to main page
|
|
262
|
+
const IDE_LEVEL_SCRIPTS = ['listModes', 'setMode', 'listModels', 'setModel'];
|
|
263
|
+
if (IDE_LEVEL_SCRIPTS.includes(scriptName)) {
|
|
264
|
+
// Try session frame first (some extensions embed mode selector in their webview)
|
|
265
|
+
if (targetSessionId) {
|
|
266
|
+
try {
|
|
267
|
+
result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
|
|
268
|
+
// Check if result indicates "not found" — fallback to main page
|
|
269
|
+
const parsed = typeof result === 'string' ? JSON.parse(result) : result;
|
|
270
|
+
const notFound = parsed?.error?.includes('not found') || parsed?.error?.includes('no root');
|
|
271
|
+
if (notFound) {
|
|
272
|
+
LOG.info('Command', `[ExtScript] ${scriptName} not found in session frame → trying IDE main page`);
|
|
273
|
+
result = await cdp.evaluate(scriptCode, 30000);
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
LOG.info('Command', `[ExtScript] ${scriptName} session frame failed → trying IDE main page`);
|
|
277
|
+
result = await cdp.evaluate(scriptCode, 30000);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
LOG.info('Command', `[ExtScript] ${scriptName} no session → trying IDE main page`);
|
|
281
|
+
result = await cdp.evaluate(scriptCode, 30000);
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
if (!targetSessionId) {
|
|
285
|
+
return { success: false, error: `No active session found for ${agentType}` };
|
|
286
|
+
}
|
|
287
|
+
result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
|
|
254
288
|
}
|
|
255
|
-
result = await cdp.evaluateInSessionFrame(targetSessionId, scriptCode);
|
|
256
289
|
} else if (hasWebviewScript && cdp.evaluateInWebviewFrame) {
|
|
257
290
|
const matchText = provider.webviewMatchText;
|
|
258
291
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
package/src/daemon/dev-server.ts
CHANGED
|
@@ -339,15 +339,29 @@ export class DevServer {
|
|
|
339
339
|
}
|
|
340
340
|
this.log(`Exec script length: ${scriptCode.length}, first 50 chars: ${scriptCode.slice(0, 50)}...`);
|
|
341
341
|
|
|
342
|
-
// Execute
|
|
343
|
-
const isWebviewScript =
|
|
342
|
+
// Execute based on provider category
|
|
343
|
+
const isWebviewScript = scriptName.toLowerCase().includes('webview');
|
|
344
344
|
let raw: any;
|
|
345
|
-
if (isWebviewScript) {
|
|
345
|
+
if (provider.category === 'extension' && !isWebviewScript) {
|
|
346
|
+
// Extension scripts: prefer session frame (agent webview) — matching agent-stream poller behavior
|
|
347
|
+
const sessions = cdp.getAgentSessions();
|
|
348
|
+
let sessionId: string | null = null;
|
|
349
|
+
for (const [sid, target] of sessions) {
|
|
350
|
+
if (target.agentType === type) { sessionId = sid; break; }
|
|
351
|
+
}
|
|
352
|
+
if (sessionId) {
|
|
353
|
+
raw = await cdp.evaluateInSessionFrame(sessionId, scriptCode);
|
|
354
|
+
} else if (cdp.evaluateInWebviewFrame) {
|
|
355
|
+
// Fallback: try evaluateInWebviewFrame
|
|
356
|
+
const matchText = provider.webviewMatchText;
|
|
357
|
+
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
358
|
+
raw = await cdp.evaluateInWebviewFrame(scriptCode, matchFn);
|
|
359
|
+
} else {
|
|
360
|
+
raw = await cdp.evaluate(scriptCode, 30000);
|
|
361
|
+
}
|
|
362
|
+
} else if (isWebviewScript && cdp.evaluateInWebviewFrame) {
|
|
346
363
|
const matchText = provider.webviewMatchText;
|
|
347
364
|
const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
|
|
348
|
-
if (!cdp.evaluateInWebviewFrame) {
|
|
349
|
-
throw new Error(`CDP manager does not support evaluateInWebviewFrame`);
|
|
350
|
-
}
|
|
351
365
|
raw = await cdp.evaluateInWebviewFrame(scriptCode, matchFn);
|
|
352
366
|
} else {
|
|
353
367
|
raw = await cdp.evaluate(scriptCode, 30000);
|
package/src/status/reporter.ts
CHANGED
|
@@ -221,7 +221,7 @@ export class DaemonStatusReporter {
|
|
|
221
221
|
LOG.debug('P2P', `sent (${JSON.stringify(payload).length} bytes)`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
// ═══ Server transmit (minimal routing meta only
|
|
224
|
+
// ═══ Server transmit (minimal routing meta only) ═══
|
|
225
225
|
if (opts?.p2pOnly) return;
|
|
226
226
|
const wsPayload = {
|
|
227
227
|
daemonMode: true,
|