Skip to content

refactor(byoa): one source of truth for the engines a user can pick - #116

Merged
WhichPaths merged 1 commit into
yetone:mainfrom
WhichPaths:refactor/engine-lists-single-source
Aug 30, 2026
Merged

refactor(byoa): one source of truth for the engines a user can pick#116
WhichPaths merged 1 commit into
yetone:mainfrom
WhichPaths:refactor/engine-lists-single-source

Conversation

@WhichPaths

Copy link
Copy Markdown
Collaborator

#101 had to follow #99 with four one-line additions, and they are all the same mistake:

-const PAIRABLE_ENGINES: ReadonlySet<string> = new Set(['claude', …, 'pi'])
+const PAIRABLE_ENGINES: ReadonlySet<string> = new Set(['claude', …, 'pi', 'gemini'])

-const [engine, setEngine] = useState<'claude' | … | 'pi'>('claude')
+const [engine, setEngine] = useState<'claude' | … | 'pi' | 'gemini'>('claude')

-{([['claude', 'Claude Code'], …, ['pi', 'pi']] as const).map(([id, label]) => (
+{([['claude', 'Claude Code'], …, ['pi', 'pi'], ['gemini', 'Gemini']] as const).map(([id, label]) => (

(the last two twice — once in MeView, once in Onboarding)

Gemini had an adapter, was detected, was labelled, and still could not be paired or selected, because four lists that are invisible from the adapter had not been told about it. Nothing errored.

The sharpest version of it: MeView already imports RUNNABLE_ENGINE_IDS and engineLabel — and then inlines its own copy of both, ten lines away.

Removing the copies rather than guarding them

src/lib/engines.ts declares the runnable engines once and derives the three shapes callers actually need:

export const RUNNABLE_ENGINES = ['claude', 'codex', 'grok', 'cursor', 'opencode', 'pi', 'gemini'] as const
export type RunnableEngineId = typeof RUNNABLE_ENGINES[number]
export const RUNNABLE_ENGINE_IDS: ReadonlySet<string> = new Set<string>(RUNNABLE_ENGINES)

Both pickers become:

{RUNNABLE_ENGINES.map((id) => (
  <button key={id} onClick={() => setEngine(id)} >{engineLabel(id)}</button>
))}

and both useState unions become useState<RunnableEngineId>('claude'). There is no longer a list to forget.

The one that can't be derived, handed to the compiler

PAIRABLE_ENGINES is a server-side policy set ("engines a paired, non-cloud computer may advertise"), so it isn't simply the UI list. It becomes a Record over the engine union instead of a Set literal:

const PAIRABLE: Record<Exclude<EngineId, 'managed'>, true> = {
  claude: true, codex: true, grok: true, cursor: true, opencode: true, pi: true, gemini: true,
}
const PAIRABLE_ENGINES: ReadonlySet<string> = new Set<string>(Object.keys(PAIRABLE))

Adding an id to EngineId now fails to compile until it is classified. Verified by adding 'qwen' to the union:

server/src/agents/computer/registry.ts(54,7): error TS2741: Property 'qwen' is missing in type
'{ claude: true; codex: true; grok: true; cursor: true; opencode: true; pi: true; gemini: true; }'
but required in type 'Record<"claude" | … | "qwen", true>'.

That is better than a CI check for this one: it fails in the editor, before the commit exists. A new Set([...]) accepted the incomplete list in silence, and the engine left out could be detected and displayed but never paired.

One visible change

The picker rendered pi as "pi" while every other surface — including the engine list eight lines below it in the same file — renders engineLabel('pi') = "Pi". They now agree.

Checks

tsc (app + server), biome lint ., npm run build, both guards, and 39/39 across the engine test files. No behaviour change beyond the label, and the engine sets are byte-identical to what #101 left.

Complements #114, which guards the lists that can't be collapsed. This one deletes the ones that can.

yetone#101 had to follow yetone#99 with four one-line additions: PAIRABLE_ENGINES in the
computer registry, and — in both MeView and Onboarding — a hardcoded engine
union and a hardcoded [id, label] array for the picker. Gemini had an
adapter, was detected, was labelled, and still could not be paired or
selected, because four lists nobody could see from the adapter had not been
told about it.

MeView already imported RUNNABLE_ENGINE_IDS and engineLabel, and still
inlined its own copy of both. That is the whole bug in one line.

src/lib/engines.ts now declares the runnable engines once and derives the
three shapes callers need: RUNNABLE_ENGINES (ordered, for pickers),
RunnableEngineId (for the state that holds a choice), and the existing
RUNNABLE_ENGINE_IDS set. Both pickers map over the list and call
engineLabel(), so there is nothing left to forget.

PAIRABLE_ENGINES becomes a Record over the engine union rather than a Set
literal, which moves the check from review to the compiler: adding an id to
EngineId now fails to compile until it is classified. Verified by adding
'qwen' to the union — `Property 'qwen' is missing in type ...`. A Set
literal accepted the incomplete list silently, and an engine left out of it
could be detected and displayed but never actually paired.

One visible change: the picker labelled pi as "pi" while every other
surface renders engineLabel('pi') = "Pi". They now agree.
@WhichPaths

Copy link
Copy Markdown
Collaborator Author

Note on ordering with the other open PRs, since three of these touch the engine lists:

Happy to rebase whichever order suits you.

@WhichPaths

Copy link
Copy Markdown
Collaborator Author

Correction to my note above: I said I had run #114's guard against this branch. I had not, and when I did, it failed — it anchored on RUNNABLE_ENGINE_IDS = new Set([...]), which this PR replaces with a derived form.

That is the guard behaving as designed (it reports anchor not found rather than passing vacuously), but it would have made the guard the thing blocking a change that removes the duplication it exists to police. I have pushed a commit to #114 so it reads either shape, and verified it against all three branches — this one, main, and #113.

Sorry for the unchecked claim.

WhichPaths added a commit to WhichPaths/cumora that referenced this pull request Aug 30, 2026
The guard anchored on `RUNNABLE_ENGINE_IDS = new Set([...])`. yetone#116 collapses
that duplicate by deriving the set from an ordered `RUNNABLE_ENGINES` tuple,
and the guard then reported "anchor not found" — correctly, by its own
design, but it would have made this the thing blocking a change that removes
the very duplication it exists to police.

It now reads whichever shape is present. Verified against all three open
branches: main's Set literal, yetone#116's tuple, and yetone#113's added engine.
@WhichPaths
WhichPaths merged commit 1343da0 into yetone:main Aug 30, 2026
6 checks passed
WhichPaths added a commit to WhichPaths/cumora that referenced this pull request Aug 31, 2026
Finishes yetone#75's list. `qwen` was already in ENGINE_LABEL / ENGINE_BIN and
ENGINE_VERSION_SPECS, so the Computers tab could report its version and
Cumora still could not wake it.

The adapter is thin for a reason worth stating: Qwen Code is a Gemini CLI
fork and shares its flags (-o stream-json, -p, -r, -m), but it does NOT
share Gemini's output. It emits Claude Code's envelope —
{type:'assistant',session_id,message:{model,content,usage}} closing on
{type:'result',subtype,is_error,num_turns,usage} — which is exactly the
shape spawnEngine already sniffs. So the wake path needs no parser of its
own, and gets session id, terminating usage, real model and one ledger hop
per assistant message for free.

An adapter derived from the Gemini one would have parsed nothing. That is
also why triage needs the only new code here: `-o json` returns those same
events wrapped in a JSON array rather than Claude's {result,usage} object,
so there is no envelope to unwrap and the reply has to be assembled from
the assistant text blocks.

Triage runs --safe-mode, qwen's own switch for "ignore every
customization": context files, hooks, extensions, skills and MCP servers
stay unloaded. It leaves auth alone, and --yolo is an argv flag that still
wins over it, so nothing can stall waiting for an approval nobody is there
to give.

Every claim above was checked against a real @qwen-code/qwen-code 0.22.3,
including that `-y` genuinely sets approval mode (its help dropped the
entry in the subcommand restructure, but the headless yolo warning fires
only when getApprovalMode() === 'yolo', and it fires).

Rebased onto the BYOA sandboxing work. Qwen lands in the compatibility
tier, and needs no code to put it there: SANDBOXED_ENGINE_IDS is
['claude','codex'], so runnableEngineIds() already excludes qwen unless
CUMORA_BYOA_ALLOW_UNSANDBOXED=1 is set. Verified — default resolves to
claude, codex; with the opt-in, claude, codex, gemini, qwen. The docs table
gains a Qwen column in that vocabulary, and the agent-cli prose is left as
main rewrote it, since it deliberately stopped enumerating engines.

byoa-qwen goes in the shared BYOA_SOURCES whitelist: normalizeByoaSource
falls back to 'byoa-claude' for anything unknown, so omitting it would have
attributed every qwen run's spend to Claude rather than failing loudly. The
PAIRABLE record from yetone#116 caught the same class of omission at compile time
during this rebase, which is what it was added for.

Also refreshes a redetect fixture that used 'qwen' as its example of an
engine with no adapter — true until this commit.
yetone pushed a commit that referenced this pull request Aug 31, 2026
Finishes #75's list. `qwen` was already in ENGINE_LABEL / ENGINE_BIN and
ENGINE_VERSION_SPECS, so the Computers tab could report its version and
Cumora still could not wake it.

The adapter is thin for a reason worth stating: Qwen Code is a Gemini CLI
fork and shares its flags (-o stream-json, -p, -r, -m), but it does NOT
share Gemini's output. It emits Claude Code's envelope —
{type:'assistant',session_id,message:{model,content,usage}} closing on
{type:'result',subtype,is_error,num_turns,usage} — which is exactly the
shape spawnEngine already sniffs. So the wake path needs no parser of its
own, and gets session id, terminating usage, real model and one ledger hop
per assistant message for free.

An adapter derived from the Gemini one would have parsed nothing. That is
also why triage needs the only new code here: `-o json` returns those same
events wrapped in a JSON array rather than Claude's {result,usage} object,
so there is no envelope to unwrap and the reply has to be assembled from
the assistant text blocks.

Triage runs --safe-mode, qwen's own switch for "ignore every
customization": context files, hooks, extensions, skills and MCP servers
stay unloaded. It leaves auth alone, and --yolo is an argv flag that still
wins over it, so nothing can stall waiting for an approval nobody is there
to give.

Every claim above was checked against a real @qwen-code/qwen-code 0.22.3,
including that `-y` genuinely sets approval mode (its help dropped the
entry in the subcommand restructure, but the headless yolo warning fires
only when getApprovalMode() === 'yolo', and it fires).

Rebased onto the BYOA sandboxing work. Qwen lands in the compatibility
tier, and needs no code to put it there: SANDBOXED_ENGINE_IDS is
['claude','codex'], so runnableEngineIds() already excludes qwen unless
CUMORA_BYOA_ALLOW_UNSANDBOXED=1 is set. Verified — default resolves to
claude, codex; with the opt-in, claude, codex, gemini, qwen. The docs table
gains a Qwen column in that vocabulary, and the agent-cli prose is left as
main rewrote it, since it deliberately stopped enumerating engines.

byoa-qwen goes in the shared BYOA_SOURCES whitelist: normalizeByoaSource
falls back to 'byoa-claude' for anything unknown, so omitting it would have
attributed every qwen run's spend to Claude rather than failing loudly. The
PAIRABLE record from #116 caught the same class of omission at compile time
during this rebase, which is what it was added for.

Also refreshes a redetect fixture that used 'qwen' as its example of an
engine with no adapter — true until this commit.

Co-authored-by: Xialie Zhuang <62231346+Lieisyourlie@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant