refactor(byoa): one source of truth for the engines a user can pick - #116
Merged
WhichPaths merged 1 commit intoAug 30, 2026
Merged
Conversation
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.
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. |
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 That is the guard behaving as designed (it reports 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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#101 had to follow #99 with four one-line additions, and they are all the same mistake:
(the last two twice — once in
MeView, once inOnboarding)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:
MeViewalready importsRUNNABLE_ENGINE_IDSandengineLabel— and then inlines its own copy of both, ten lines away.Removing the copies rather than guarding them
src/lib/engines.tsdeclares the runnable engines once and derives the three shapes callers actually need:Both pickers become:
and both
useStateunions becomeuseState<RunnableEngineId>('claude'). There is no longer a list to forget.The one that can't be derived, handed to the compiler
PAIRABLE_ENGINESis 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:Adding an id to
EngineIdnow fails to compile until it is classified. Verified by adding'qwen'to the union: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
pias "pi" while every other surface — including the engine list eight lines below it in the same file — rendersengineLabel('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.