9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
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
333 lines
14 KiB
JavaScript
333 lines
14 KiB
JavaScript
// Global UI commands for Qoder.
|
|
//
|
|
// sidebar-toggle — Collapse Quest List (⌘B)
|
|
// open-panel — Open Panel (⌥⌘B) — bottom panel
|
|
// search — Search (⌘P) palette
|
|
// settings — Settings
|
|
// knowledge — open Knowledge view
|
|
// marketplace — open Marketplace
|
|
// credits — Credits Usage
|
|
// view-all — View all quests
|
|
// add-workspace — Add Workspace
|
|
// account — read user info (the "leo"-style button at bottom)
|
|
// more-actions — open More Actions menu + list items
|
|
|
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import {
|
|
ArgumentError,
|
|
CommandExecutionError,
|
|
EmptyResultError,
|
|
} from '@jackwener/opencli/errors';
|
|
import { IS_VISIBLE_JS, clickByTextScript, clickFirstScript, evaluateQoder, parsePositiveInt, requireArrayResult } from './_utils.js';
|
|
|
|
// -------- sidebar-toggle --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'sidebar-toggle',
|
|
access: 'write',
|
|
description: 'Collapse / Expand the Qoder Quest List sidebar (⌘B).',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Collapse Quest List', 'Expand Quest List']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'sidebar-toggle failed', '');
|
|
return [{ Status: `clicked: ${res.matched}` }];
|
|
},
|
|
});
|
|
|
|
// -------- open-panel --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'open-panel',
|
|
access: 'write',
|
|
description: 'Open / close the Qoder bottom panel (Output / Terminal / Debug Console). ⌥⌘B equivalent.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Open Panel', 'Close Panel']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'open-panel failed', '');
|
|
return [{ Status: `clicked: ${res.matched}` }];
|
|
},
|
|
});
|
|
|
|
// -------- search --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'search',
|
|
access: 'read',
|
|
description: 'Open Qoder Search palette (⌘P), type a query, return matched options.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'query', positional: true, required: true, help: 'Search text' },
|
|
{ name: 'limit', type: 'int', required: false, default: 20 },
|
|
],
|
|
columns: ['Index', 'Item'],
|
|
func: async (page, kwargs) => {
|
|
const query = String(kwargs?.query || '').trim();
|
|
if (!query) throw new ArgumentError('query', 'is required');
|
|
const limit = parsePositiveInt(kwargs?.limit, 20, '--limit');
|
|
const openRes = await evaluateQoder(page, clickByTextScript(['Search']));
|
|
if (!openRes?.ok) throw new CommandExecutionError(openRes?.reason || 'search open failed', '');
|
|
await page.wait(0.5);
|
|
// Type into the visible input (usually the most-recently mounted one).
|
|
const fillRes = await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
const inputs = Array.from(document.querySelectorAll('input[type="text"], input[type="search"], input:not([type])')).filter(isVisible);
|
|
const input = inputs[inputs.length - 1];
|
|
if (!input) return { ok: false, reason: 'No input visible.' };
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
|
setter.call(input, ${JSON.stringify(query)});
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return { ok: true };
|
|
})()`);
|
|
if (!fillRes?.ok) throw new CommandExecutionError(fillRes?.reason || 'search type failed', '');
|
|
await page.wait(0.8);
|
|
const items = requireArrayResult(await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
// Search palette results: prefer [role=option], else any clickable row in the modal.
|
|
const opts = Array.from(document.querySelectorAll('[role="option"], [role="menuitem"]')).filter(isVisible);
|
|
const titles = opts.map((o) => {
|
|
// Codex-style fix: text may be in per-char spans; use textContent.
|
|
const titleEl = o.querySelector('.truncate, [class*="title"i]') || o;
|
|
return (titleEl.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 200);
|
|
}).filter(Boolean);
|
|
return [...new Set(titles)];
|
|
})()`), 'qoder search');
|
|
try { await page.evaluate(`document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));`); } catch {}
|
|
if (!items.length) {
|
|
throw new EmptyResultError('qoder search', `No items matched "${query}".`);
|
|
}
|
|
return items.slice(0, limit).map((t, i) => ({ Index: i + 1, Item: t }));
|
|
},
|
|
});
|
|
|
|
// -------- settings --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'settings',
|
|
access: 'write',
|
|
description: 'Click the Settings button in the Qoder sidebar.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Settings']));
|
|
if (!res?.ok) {
|
|
// Fallback: try aria-label.
|
|
const ariaRes = await evaluateQoder(page, clickFirstScript(['button[aria-label="Settings"]']));
|
|
if (!ariaRes?.ok) throw new CommandExecutionError('Settings button not found', '');
|
|
}
|
|
await page.wait(0.5);
|
|
return [{ Status: 'clicked' }];
|
|
},
|
|
});
|
|
|
|
// -------- knowledge --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'knowledge',
|
|
access: 'write',
|
|
description: 'Open the Knowledge view (Qoder\'s personal/team knowledge base).',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Knowledge']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'Knowledge button not found', '');
|
|
return [{ Status: 'clicked' }];
|
|
},
|
|
});
|
|
|
|
// -------- marketplace --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'marketplace',
|
|
access: 'write',
|
|
description: 'Open the Qoder Marketplace.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Marketplace']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'Marketplace button not found', '');
|
|
return [{ Status: 'clicked' }];
|
|
},
|
|
});
|
|
|
|
// -------- credits --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'credits',
|
|
access: 'read',
|
|
description: 'Click "Credits Usage" and return the credits-usage display text.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Field', 'Value'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Credits Usage']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'Credits Usage not visible', '');
|
|
await page.wait(0.5);
|
|
// Try to read whatever appears.
|
|
const info = await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
// Find any dialog/popover that appeared.
|
|
const popovers = Array.from(document.querySelectorAll('[role="dialog"], [class*="popover"i], [class*="popup"i]')).filter(isVisible);
|
|
if (!popovers.length) return null;
|
|
const pop = popovers[popovers.length - 1];
|
|
return (pop.innerText || pop.textContent || '').trim().replace(/\\s+/g, ' ').slice(0, 600);
|
|
})()`);
|
|
try { await page.evaluate(`document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));`); } catch {}
|
|
return [
|
|
{ Field: 'Status', Value: 'clicked' },
|
|
{ Field: 'Info', Value: info || '(no popover detected after click)' },
|
|
];
|
|
},
|
|
});
|
|
|
|
// -------- view-all --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'view-all',
|
|
access: 'write',
|
|
description: 'Click "View all" to show all Quests.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['View all']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'View all button not visible', '');
|
|
return [{ Status: 'clicked' }];
|
|
},
|
|
});
|
|
|
|
// -------- add-workspace --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'add-workspace',
|
|
access: 'write',
|
|
description: 'Click "Add Workspace" — opens the folder picker. Note: this opens a system file-picker dialog that Qoder controls; the actual folder selection must be done in the UI by the user.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Status'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['Add Workspace']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'Add Workspace button not found', '');
|
|
return [{ Status: 'clicked — folder picker opened (manual selection required)' }];
|
|
},
|
|
});
|
|
|
|
// -------- account --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'account',
|
|
access: 'read',
|
|
description: 'Click the account button (username) in the Qoder sidebar and return the visible account dropdown items.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [
|
|
{ name: 'username', required: false, help: 'Username text shown in the sidebar (default: tries common short labels)' },
|
|
],
|
|
columns: ['Field', 'Value'],
|
|
func: async (page, kwargs) => {
|
|
// The account button label is the user's display name. We try a few
|
|
// common patterns: if --username is passed we use it, else we look
|
|
// for any short button text that doesn't match other known controls.
|
|
let label = String(kwargs?.username || '').trim();
|
|
if (!label) {
|
|
// Heuristic discovery.
|
|
label = await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
const btns = Array.from(document.querySelectorAll('button')).filter(isVisible);
|
|
const known = new Set(['Pin', 'Copy', 'New Quest', 'Search', 'Settings', 'View all', 'Knowledge', 'Marketplace', 'Credits Usage', 'Open Editor', 'Add Workspace', 'More Actions', 'Send message', 'Prompt Enhance', 'Voice input', 'button']);
|
|
const candidate = btns.find((b) => {
|
|
const tx = (b.innerText || '').trim();
|
|
if (!tx || tx.length > 30 || tx.includes('⌘') || known.has(tx)) return false;
|
|
if (/^(New|Open|Add|Save|Edit|Cancel|OK|Close)/.test(tx)) return false;
|
|
return true;
|
|
});
|
|
return candidate ? (candidate.innerText || '').trim() : '';
|
|
})()`);
|
|
}
|
|
if (!label) {
|
|
throw new CommandExecutionError('No account button detected. Pass --username <name> explicitly.', '');
|
|
}
|
|
// Click the username button
|
|
const clickRes = await evaluateQoder(page, clickByTextScript([label], { exact: true, maxLen: 60 }));
|
|
if (!clickRes?.ok) throw new CommandExecutionError(`Click on account button "${label}" failed`, '');
|
|
await page.wait(0.5);
|
|
const items = requireArrayResult(await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
const popovers = Array.from(document.querySelectorAll('[role="menu"], [role="dialog"], [class*="popover"i], [class*="dropdown"i]')).filter(isVisible)
|
|
.filter((el) => { const r = el.getBoundingClientRect(); return r.width < 500 && r.height < 600; });
|
|
if (!popovers.length) return [];
|
|
const pop = popovers[popovers.length - 1];
|
|
return Array.from(pop.querySelectorAll('button, [role="menuitem"], a'))
|
|
.filter(isVisible)
|
|
.map((b) => (b.innerText || b.textContent || '').trim().replace(/\\s+/g, ' '))
|
|
.filter(Boolean);
|
|
})()`), 'qoder account');
|
|
try { await page.evaluate(`document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));`); } catch {}
|
|
const rows = [{ Field: 'Username', Value: label }];
|
|
(items || []).slice(0, 20).forEach((item, i) => {
|
|
rows.push({ Field: `Item[${i + 1}]`, Value: item });
|
|
});
|
|
return rows;
|
|
},
|
|
});
|
|
|
|
// -------- more-actions --------
|
|
cli({
|
|
site: 'qoder',
|
|
name: 'more-actions',
|
|
access: 'read',
|
|
description: 'Click the "More Actions" button and list its menu items.',
|
|
domain: 'localhost',
|
|
strategy: Strategy.UI,
|
|
browser: true,
|
|
args: [],
|
|
columns: ['Index', 'Item'],
|
|
func: async (page) => {
|
|
const res = await evaluateQoder(page, clickByTextScript(['More Actions']));
|
|
if (!res?.ok) throw new CommandExecutionError(res?.reason || 'More Actions button not found', '');
|
|
await page.wait(0.4);
|
|
const items = requireArrayResult(await evaluateQoder(page, `(() => {
|
|
${IS_VISIBLE_JS}
|
|
const popovers = Array.from(document.querySelectorAll('[role="menu"], [role="dialog"], [class*="popover"i], [class*="menu"i]')).filter(isVisible)
|
|
.filter((el) => { const r = el.getBoundingClientRect(); return r.width < 500 && r.height < 600; });
|
|
if (!popovers.length) return [];
|
|
const pop = popovers[popovers.length - 1];
|
|
return Array.from(pop.querySelectorAll('[role="menuitem"], button'))
|
|
.filter(isVisible)
|
|
.map((b) => (b.innerText || b.textContent || '').trim().replace(/\\s+/g, ' '))
|
|
.filter(Boolean);
|
|
})()`), 'qoder more-actions');
|
|
try { await page.evaluate(`document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));`); } catch {}
|
|
if (!items.length) {
|
|
throw new EmptyResultError('qoder more-actions', 'Menu opened but no items detected.');
|
|
}
|
|
return items.map((it, i) => ({ Index: i + 1, Item: it }));
|
|
},
|
|
});
|