9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
135 lines
6.5 KiB
JavaScript
135 lines
6.5 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { CommandExecutionError, selectorError } from '@jackwener/opencli/errors';
|
|
|
|
async function readCurrentModel(page) {
|
|
const current = await page.evaluate(`(function() {
|
|
const trigger = document.querySelector('.core-model-select-trigger');
|
|
return trigger ? (trigger.textContent || '').trim() : '';
|
|
})()`);
|
|
return typeof current === 'string' ? current.trim() : '';
|
|
}
|
|
|
|
cli({
|
|
site: 'trae-solo',
|
|
name: 'model',
|
|
access: 'write',
|
|
description: 'Read or switch the current AI model in TRAE SOLO. Without arguments, reports the current model. With <name> argument (substring, case-insensitive), switches to a matching model. Pass --list to enumerate available models.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'name', required: false, positional: true, help: 'Target model name (substring match, case-insensitive). Omit to read current.' },
|
|
{ name: 'list', type: 'boolean', default: false, help: 'List all available models (does not switch)' },
|
|
],
|
|
columns: ['Status', 'Model'],
|
|
func: async (page, kwargs) => {
|
|
const name = String(kwargs.name || '').trim().toLowerCase();
|
|
const listOnly = kwargs.list === true || kwargs.list === 'true';
|
|
|
|
// Read current model from the composer trigger
|
|
const current = await readCurrentModel(page);
|
|
if (!current) {
|
|
throw selectorError('TRAE SOLO model trigger (.core-model-select-trigger). Make sure a chat task is open (not the project-list view).');
|
|
}
|
|
|
|
// List or switch — both require opening the menu
|
|
if (listOnly || name) {
|
|
const namejson = JSON.stringify(name);
|
|
|
|
// Stage 1 (eval): open the trigger + enumerate option labels.
|
|
const stage1 = await page.evaluate(`(async () => {
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const trigger = document.querySelector('.core-model-select-trigger');
|
|
if (!trigger) return { ok: false, reason: 'model trigger gone' };
|
|
const r = trigger.getBoundingClientRect();
|
|
const init = {
|
|
bubbles: true, cancelable: true, button: 0, buttons: 1,
|
|
clientX: Math.round(r.left + Math.min(r.width / 2, 20)),
|
|
clientY: Math.round(r.top + Math.min(r.height / 2, 10)),
|
|
};
|
|
trigger.dispatchEvent(new PointerEvent('pointerdown', { ...init, pointerType: 'mouse' }));
|
|
trigger.dispatchEvent(new MouseEvent('mousedown', init));
|
|
trigger.dispatchEvent(new PointerEvent('pointerup', { ...init, pointerType: 'mouse' }));
|
|
trigger.dispatchEvent(new MouseEvent('mouseup', init));
|
|
trigger.dispatchEvent(new MouseEvent('click', init));
|
|
|
|
let opts = [];
|
|
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
await wait(80);
|
|
opts = Array.from(document.querySelectorAll('.core-model-select-model-item[role="option"]'))
|
|
.filter((el) => el instanceof HTMLElement && el.offsetParent);
|
|
if (opts.length) break;
|
|
}
|
|
if (!opts.length) {
|
|
return { ok: false, reason: 'Model menu did not open.' };
|
|
}
|
|
const labels = opts.map((o) => {
|
|
const nameEl = o.querySelector('.core-model-select-model-item-name');
|
|
return ((nameEl ? nameEl.textContent : o.textContent) || '').trim();
|
|
});
|
|
return { ok: true, labels };
|
|
})()`);
|
|
|
|
if (!stage1.ok) {
|
|
throw new CommandExecutionError(stage1.reason, stage1.detail || '');
|
|
}
|
|
|
|
if (listOnly) {
|
|
try { await page.evaluate('document.body.click()'); } catch {}
|
|
return stage1.labels.map((m) => ({ Status: m === current ? 'Active' : 'Available', Model: m }));
|
|
}
|
|
|
|
// Stage 2 (page.click): use real CDP Input.dispatchMouseEvent —
|
|
// Trae's model menu items don't fire React onSelect from synthetic
|
|
// pointer events alone. Match by nth-of-type derived from labels.
|
|
const idx = stage1.labels.findIndex((l) => l.toLowerCase().includes(name));
|
|
if (idx < 0) {
|
|
try { await page.evaluate('document.body.click()'); } catch {}
|
|
throw new CommandExecutionError(
|
|
`No model matched: '${name}'`,
|
|
'available=' + JSON.stringify(stage1.labels),
|
|
);
|
|
}
|
|
const chosenLabel = stage1.labels[idx];
|
|
const clickSelector = `.core-model-select-model-item[role="option"]:nth-of-type(${idx + 1})`;
|
|
try {
|
|
await page.click(clickSelector);
|
|
} catch (err) {
|
|
// Some Trae model rows wrap option in another element; fall back
|
|
// to JS dispatch on the nth visible option.
|
|
const fallbackClicked = await page.evaluate(`(function(i) {
|
|
const opts = Array.from(document.querySelectorAll('.core-model-select-model-item[role="option"]'))
|
|
.filter((el) => el.offsetParent);
|
|
const target = opts[i];
|
|
if (!target) return false;
|
|
const r = target.getBoundingClientRect();
|
|
const init = { bubbles: true, cancelable: true, button: 0, buttons: 1,
|
|
clientX: Math.round(r.left + r.width / 2),
|
|
clientY: Math.round(r.top + r.height / 2) };
|
|
target.dispatchEvent(new PointerEvent('pointerdown', { ...init, pointerType: 'mouse' }));
|
|
target.dispatchEvent(new MouseEvent('mousedown', init));
|
|
target.dispatchEvent(new PointerEvent('pointerup', { ...init, pointerType: 'mouse' }));
|
|
target.dispatchEvent(new MouseEvent('mouseup', init));
|
|
target.dispatchEvent(new MouseEvent('click', init));
|
|
return true;
|
|
})(${idx})`);
|
|
if (!fallbackClicked) {
|
|
throw new CommandExecutionError('Click on model option failed.', `model=${chosenLabel}`);
|
|
}
|
|
}
|
|
await page.wait(0.6);
|
|
const after = await readCurrentModel(page);
|
|
if (!after || !after.toLowerCase().includes(name)) {
|
|
throw new CommandExecutionError(
|
|
`Model click did not verify selected model "${chosenLabel}".`,
|
|
after ? `current=${after}` : 'model trigger was unreadable after click',
|
|
);
|
|
}
|
|
return [{ Status: 'switched', Model: after }];
|
|
}
|
|
|
|
// Just read the current.
|
|
return [{ Status: 'Active', Model: current }];
|
|
},
|
|
});
|