Repository navigation
fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests - #159
BIackFIame wants to merge 15 commits into
Conversation
The Kimi provider runs Kimi Code CLI, which keeps its configuration and OAuth sign-in in KIMI_CODE_HOME (default ~/.kimi-code); the older kimi-cli uses KIMI_SHARE_DIR (default ~/.kimi). The isolation layer only knew KIMI_HOME, which neither CLI reads. So: - a Kimi launch whose home was moved with KIMI_CODE_HOME (an account home a launch plugin hands it, e.g. one sign-in of an account pool) did not get that folder as its own writable state, and - other CLIs inside the layer could read ~/.kimi-code, where Kimi Code's refresh token lives. Kimi's own folders are now KIMI_CODE_HOME (or ~/.kimi-code), KIMI_SHARE_DIR (or ~/.kimi), KIMI_HOME when set, and the usual named folders; all three variables move Kimi's home. Nothing else changes.
…atus hooks and tools Grok Build reads hooks only from GROK_HOME/hooks and Kimi Code only from KIMI_CODE_HOME/config.toml; Kimi also reads its MCP servers (mcp.json) and permission rules there. CanvasTTY wrote its lifecycle hook overlays and Kimi's browser/agents configuration into the default homes only, so a card a launch contributor started in its own account home (an account pool) ran without working/idle/needs_approval status and without the canvastty_browser / canvastty_agents tools. When a launch contributor sets GROK_HOME / KIMI_CODE_HOME to an account home below its own data folder (<userData>/plugin-data/<pluginId>, from the host's record), the launch pipeline reports it as the launch's account home and the same overlays are written there instead, shared by the cards in that home and removed when the last one ends. A variable pointing anywhere else, including another plugin's data, leaves the launch as before. Inside plugin data every folder from plugin-data down and every file CanvasTTY writes must be real (no symbolic link), or the launch is refused. The overlays keep the existing lock, journal, backup and marker rules, so the person's own settings are restored byte for byte. The checks live in accountHomes.ts and are shared by both overlays. Claude Code, Codex, Qwen Code and OpenCode take their hooks from the launch's arguments or environment and already work in any home.
…ts orchestrator An OpenCode subagent on a launch contributor's model keeps bash on "ask" (a launch contributor's other model keeps bash asking, like accept-edits elsewhere). When no decision service decides, OpenCode's own prompt waits in the subagent's card for the person. That part already works: the card turns needs_approval, Needs attention lists it, and wait_for_agent returns at once with the prompt in output. Two things stood in the way: - send_to_agent still typed into that card. Any prompt text ends with Enter, which answers OpenCode's prompt, so the orchestrator (on the same third-party model) could approve the command for the person. It is now refused while the subagent is needs_approval, with a reason that tells the orchestrator to ask the person and wait. - wait_for_agent returned the same pending prompt again on every call, so an orchestrator could only poll or give up. It now returns a prompt once; a later wait on the same prompt (the card has not changed since) waits for the person's answer and then for the subagent's turn, so the orchestrator carries on in the same turn once the person has answered. A new prompt is reported at once again. Unchanged: bash stays "ask" for a third-party model in every card; a decision service's deny still fails the call before OpenCode asks and its allow (from a plugin the person let allow) still answers OpenCode's prompt once; no verdict is never turned into an allow; person cards and OpenCode on its own model are untouched; no new UI. The test runs CanvasTTY's real OpenCode plugin, runtime gateway, decision hooks, TerminalManager and orchestration tools: allow answers the prompt, deny fails the call, no verdict reaches Needs attention and the orchestrator, send_to_agent writes nothing, and the person's answer resumes the subagent and the orchestrator's wait.
…s a subagent's approval prompt
Asks are answered by the person, or by a decision service the person
allowed, never by the orchestrator. The canvastty_agents tools already
refuse send_to_agent while a subagent is needs_approval. The control
endpoint did not: on an Orchestrator session's own connection, choose
answered Codex's approval prompt ("1. Yes, proceed") and dismiss answered
it with Escape ("No, and tell Codex what to do differently").
On an orchestrator's connection, choose, dismiss and send are now refused
with NEEDS_PERSON (not retryable, nothing is written) while the worker
is needs_approval, the status each CLI's own approval signal sets. For
Codex's choose and dismiss the menu is also checked: Codex can draw its
approval prompt before its PermissionRequest hook's report arrives, and
choose acts on that exact screen. codexApprovalPrompt matches Codex's
command, edits, permissions, network and MCP approval prompts, not hooks
review, folder trust, model or update menus.
Unchanged: menus that are not approvals stay the orchestrator's; status,
screen and result still read; interrupt still stops its own turn (the
asked call does not run); the person's own app-wide connection, the
card's input and decision services are untouched.
Tests run the real TerminalManager, AgentControlService, gateway and
control CLI request path with an orchestrator's session grant: approval
prompts refused before and after the hook report, non-approval menus
chosen and dismissed, the person's connection still choosing, and
send_to_agent refused at every CLI's approval signal and delivered once
the person answered.
…e UI scale
Some UI chrome kept fixed pixel sizes, so Settings -> Appearance -> UI
scale (0.85-1.25) did not reach it:
- The terminal card's options menu (Don't restore this window, plugin
card actions) had a 12px font and pixel paddings while the card header
around it scaled.
- The canvas shortcut-hints panel looked the same at 0.85 and 1.25.
- In Settings -> Plugins the plugin description scaled with the settings
panel, but the permission list, the contribution rows ("App on
canvas"), the plugin title, author, version, buttons, module and
native-hook lists kept fixed 9-17px sizes, so at 1.25 one card mixed
large and small text.
The menu and the hints panel now take their font size from
calc(Npx * var(--ui-scale, 1)) and size their paddings, gaps, radii and
minimum widths in em; the hints icon is sized in em too. The plugin
settings font sizes use the same calc. At scale 1.0 every size is the
same as before. UI_CONTRACT names the menu and the hints panel among the
chrome that follows --ui-scale.
The title bar is left as is: its height is shared with the native
window chrome (macOS traffic-light position, the startup page) and with
every viewport-bound overlay through --titlebar-height.
…like the reference
The Russian canvas hints and Settings still showed English: the
wheel/pinch hint, the hard-coded "Shift + drag" key cap in the hints
panel and the shortcut reference, the canvas navigation override
description ("wheel, pinch и drag ... Drag колёсиком") and the
Settings > Canvas wheel capture strings and Off/On/Key labels.
- Translate them, reusing the existing on/off keys and adding
shortcutShiftDrag and canvasWheelCaptureKey; the hints use one word for
the drag gesture ("перетаскивание"). The group-selection label no
longer repeats its key cap (ru/en). A test checks that these ru strings
hold no Latin words besides key names.
- The wheel/pinch and navigation override rows in Settings > Keyboard
shortcuts wrap instead of cutting their longer Russian labels off.
- The hints panel showed raw bindings ("Alt+ArrowUp") while the shortcut
reference showed "Alt+↑". Both use one formatter now. It maps key by
key, so on macOS "Meta+Comma" reads "Command+," instead of the
reference's ",nd+Comma" (the Comma key was replaced inside "Command").
The canvas wheel and navigation settings' text (only Settings shows it)
moves from the startup string table to canvasNavigationText.ts, next to
Settings, so it loads with the Settings chunk; renderer-bundle-splitting
checks it stays out of the startup bundle.
When GitHub returned a token but the /user request hit the request timeout, the timeout's AbortError was taken for a cancellation: the poll stopped silently, the flow was cleared and the status stayed "pending", so Cancel did nothing. Only this flow's own abort now counts as a cancellation; a timed-out profile request ends the sign-in as failed.
…or body
A transient 429/5xx from the token endpoint with a JSON body such as
{"error":"temporarily_unavailable"} skipped the transient-status check and
ended the sign-in as an unknown provider error after one answer. These
statuses are now retried with backoff whatever the body says; slow_down
keeps its own handling.
browser_click on an engine without layout returns when element.click() answers; the fake engine reports the navigation it started afterwards. Until Page.frameNavigated arrives the tab still shows the old document as ready, so h.ready() could return before the revision advanced and the test failed under load. h.ready() now takes the revision to wait past.
"terminal window descriptors persist atomically without scrollback or environment" saves a descriptor whose cwd is process.cwd() and then checks the whole file against /buffer|environment|capability|token/. Run from a checkout whose path contains one of those words (for example a worktree named after an environments feature), the cwd alone matched and the test failed on every run. The check now leaves the descriptor's own cwd value out; everything else in the file is checked as before.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
@codex review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
3adf7f8 to
15ba2e6
Compare
|
Self-review while the bot review fails: fixed three things, each in its own commit with a regression test. Kimi's |
|
@codex review |
|
Codex Review: Something went wrong. Try again later by commenting “@codex review”. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
… delegated cards Agent cards inherited CanvasTTY's whole environment, so every agent, every subagent and every plugin-started card, isolated or not, received provider API keys, tokens and organization-level admin keys (OPENAI_ADMIN_KEY, ANTHROPIC_ADMIN_KEY) exported where the app was started. Now an agent card never inherits admin credentials (*_ADMIN_*KEY, *_ADMIN_*TOKEN) or CanvasTTY's own (CANVASTTY_GITHUB_TOKEN and any CANVASTTY_*TOKEN/KEY/SECRET/PASSWORD); the person's own top-level agent cards otherwise keep their environment, so CLIs signed in by API key keep working. Subagents and every card a plugin starts, a plain shell card included since the plugin drives it, inherit only an allowlist (path, home, user, shell, locale, terminal, temporary and XDG folders, Windows system folders, proxy and certificate variables, the CLIs' home folders); what they need beyond that comes explicitly through a launch contribution's secretEnv. The launch builders (agent runtime hooks, browser and orchestration bridge, OpenCode inline config) now read the card's filtered environment instead of the app's, so they no longer hand the person's OPENCODE_CONFIG_CONTENT to a delegated agent. A plain shell card the person starts keeps everything but CanvasTTY's own credentials. CANVASTTY_AGENT_PASS_ENV in the app's environment lists variables to keep anyway.
…'s shapes
OpenCode's decision bridge sent write, edit and multiedit all as toolName
"edit" with {file_path, content}. A decision service could not tell a
whole-file write from a partial edit, and an edit's replaced text was
dropped, so it could not see what the edit removes.
The bridge now uses Claude Code's names and shapes, which hooks and
decision services already read:
- write: Write {file_path, content}
- edit: Edit {file_path, old_string, new_string, replace_all}
- multiedit: MultiEdit {file_path, edits: [{old_string, new_string,
replace_all}]}
- bash and apply_patch are unchanged.
Base protection reads file_path for Write, Edit and MultiEdit as before.
A test goes through DecisionHooks: the service gets kind "edit", the
path and both texts, and a MultiEdit outside the project is still
denied. The docs (en/ru/zh-CN, plugin-api.d.ts) list the shapes
OpenCode's tools arrive in.
15ba2e6 to
f345d96
Compare
|
Added fix 13 to this PR: agent cards no longer inherit credentials from the environment CanvasTTY was started with. Until now every agent, subagent and plugin-started card got the app's whole environment, so provider keys, tokens and
Behaviour change: a subagent that signed in only through an exported key now needs |
The HOME limit row showed the used share as a bare "9%" with a bar and a reset countdown; "used" was only in the hover/screen-reader text. Codex's own status line shows "% left", so 9 % used read as 9 % left. The row now says "9% used · 91% left" (ru «9% использовано · осталось 91%»), and the hover/screen-reader line "9% used · 91% left" («использовано 9% · осталось 91%»). Usage history's latest observation says the same instead of a bare percent. The normalizers were right: Codex app-server usedPercent and Claude utilization are the used share; a test pins both with fixture payloads.
…a way to the setting A plugin page whose service could not run because the person had not trusted its native code (or had its module turned off) only got "Plugin service is not running.". The host now refuses those requests with a fixed text; frames show it localized (ru/en), and the host notice carries an Open settings button that opens Settings -> Agents -> Extension native code (or Settings -> Plugins for a module). A trusted service that crashed or failed keeps the generic text, now followed by its last error.
…led outcomes
A started GitHub device sign-in could not be cancelled, retried poll
failures until the code expired, and ended silently when GitHub answered
access_denied or expired_token. A sign-in that could not start showed
the raw IPC error ("Error invoking remote method 'github-auth:start':
...") at the top of the Plugins page, in English in the Russian UI.
- GithubAuthService.cancelDeviceFlow() is public and exposed through the
existing github-auth IPC (main-renderer only) and preload.
- status() reports deviceFlowState: idle, pending, denied, expired,
failed or cancelled.
- access_denied, expired_token, unknown provider errors and non-transient
HTTP errors stop polling with that outcome; OAuth error bodies on HTTP
400 are read as well.
- Network errors, timeouts, HTTP 408/429/5xx and unreadable bodies back
off and retry, but five failures in a row end the flow as failed.
- slow_down keeps adding five seconds to every later poll.
- The host clock ends the flow as expired even if GitHub keeps answering
authorization_pending.
- Settings shows a Cancel button next to the pending code and a localized
status line (en, ru) for the outcome, using existing classes: denied,
expired and failed use the Settings error style, cancelled is a plain
status line. A start or cancel failure shows a localized line ("not
configured" for a build without an OAuth client); the details go to
the log.
The new sign-in lines live in PluginSettingsSection (the lazily loaded
Settings chunk), not in the startup string table; renderer-bundle-splitting
checks they stay out of the startup bundle.
f345d96 to
93cc2bb
Compare
|
Added fixes 14 and 15 to this PR, both under UI, after fixes 6 and 7:
The startup bundle is now 1,691,536 bytes (+2,134 vs main, under the 1,700,000 budget). The details are in entries 14 |
Branch
pr/bug-fixes, fifteen commits onmain(0fa6895d), one commit per fix, grouped by area, so any one can bereviewed, cherry-picked or reverted on its own. Every entry fixes behaviour that exists on
maintoday; there is no newAPI, setting or surface (fix 13 adds one opt-in environment variable,
CANVASTTY_AGENT_PASS_ENV; fix 15 adds an"Open settings" button to the existing notice). Fixes 13 to 15 were added after the PR was opened: 13 sits under
Isolation, after fixes 1 and 2; 14 and 15 sit under UI, after fixes 6 and 7. Text that only Settings shows (the
sign-in outcome lines of fix 8, and the canvas wheel/navigation settings fix 7 translates) moves out of the startup
string table into the lazily loaded Settings chunk (-2,788 bytes), and
renderer-bundle-splittingnow checks that thistext stays out of the startup bundle. Fixes 14 and 15 change what HOME and the canvas show, so their few lines stay in
the startup bundle (+4,922 bytes); the startup bundle ends 2,134 bytes above
main, under the 1,700,000 budget.Size: about 950 lines of source, 2,100 of tests, 30 of docs.
Merge order: 1 of 12 (first). It depends on nothing, and no feature PR depends on it, except the source-change PR (12),
which is rebased on top of it for a single CSS rule.
Fixes 6 to 10 were part of #149 (with its Codex-review follow-ups). #149's two plugin-page commits are
not bug fixes: they add manifest keys and will come as a feature PR. If you take this PR, #149 can be closed in its
favour; that is your call.
Isolation
1. Other CLIs inside isolation could read Kimi Code's sign-in.
fix(isolation): treat Kimi Code's KIMI_CODE_HOME as Kimi's own home~/.kimi-code,where Kimi Code keeps its OAuth refresh token. The homes of Claude Code, Codex and Grok are already hidden from each
other. Kimi launched with a moved home (
KIMI_CODE_HOME) could not write it, so it could not refresh its sign-in.To reproduce, start an isolated Codex card and read
~/.kimi-code/from it.KIMI_HOME, which neither Kimi Code (KIMI_CODE_HOME,~/.kimi-code)nor the older kimi-cli (
KIMI_SHARE_DIR,~/.kimi) reads. The rest of the core already usesKIMI_CODE_HOME.KIMI_CODE_HOMEor~/.kimi-code,KIMI_SHARE_DIRor~/.kimi, andKIMI_HOMEwhenset. All three are Kimi's own state for a Kimi launch and hidden from every other CLI.
tests/agent-isolation.test.mjs(the Kimi home is writable for Kimi and hidden from Codex; the defaultKimi folders are hidden from other CLIs;
KIMI_SHARE_DIR).2. Grok and Kimi cards in a contributed home stayed "working" and had no CanvasTTY tools.
fix(runtime): give Grok and Kimi cards in a contributed home their status hooks and toolslaunch:contributeplugin may start a CLI with its own home. The docs mention this for an account'sconfig home. Such a Grok or Kimi card never left "working", approvals were not shown,
wait_for_agenttimed out,and Kimi had no
canvastty_browser/canvastty_agents. To reproduce, have a launch contribution setGROK_HOMEor
KIMI_CODE_HOMEto a folder in its data folder and start a card.<GROK_HOME>/hooks, and Kimi Code reads them (with its MCP servers andpermission rules) only from
<KIMI_CODE_HOME>/config.toml. CanvasTTY wrote its overlays into the default homesonly.
the last one. This applies only when the home is strictly inside the data folder of the plugin that set it (the
host's record), at least
<plugin>/<home>deep. Nothing else is ever written. The existing lock, journal, backupand marker rules restore the person's settings byte for byte. No symbolic link is followed or written through;
otherwise the launch is refused with a clear message.
tests/agent-runtime-account-homes.test.mjs(14 tests): ownership, overlays with the default homeuntouched, byte-for-byte restore, crash recovery, links refused, Kimi tools, and end to end from
LaunchPipelinethrough
TerminalManagerto both bridges.contributedAccountHome). If fix(isolation, safety): keep CanvasTTY's private data out of agents' reach #148 lands first, this commit isrebuilt on it so the rule is not checked twice; that variant is ready.
13. Every agent, subagent and plugin-started card inherited the person's provider keys, tokens and admin keys.
fix(agents): keep ambient admin keys from agents and credentials from delegated cardsSo every agent, every subagent and every card a plugin started, isolated or not, got whatever was exported where the
app was started:
OPENAI_API_KEY,ANTHROPIC_API_KEY,GH_TOKEN, cloud credentials, and organization-level adminkeys such as
OPENAI_ADMIN_KEYandANTHROPIC_ADMIN_KEY(projects, members, usage; the newopenaiCLI readsOPENAI_ADMIN_KEYfrom the environment). The isolation layer restricts files, not variables, although the docs say otheragents' credentials cannot be read. The launch builders (status hooks, browser and orchestration MCP config) also
copied the person's
OPENCODE_CONFIG_CONTENT, which can hold keys, into every OpenCode card. And a plugin could starta plain terminal card (
sessions.createwith providerterminal), which kept the whole environment, and drive it.To reproduce, start CanvasTTY from a shell that exports
OPENAI_ADMIN_KEY, let an orchestrator spawn a subagent andrun
envin it.terminalEnvironment()filtered no credentials, and the agent runtime and browser bridges readprocess.envdirectly.ambientProviderKeys.ts, applied once inTerminalManager.planSpawn. Every launch builder now readsthe filtered environment, never
process.env.*_ADMIN_*KEY,*_ADMIN_*TOKEN) and CanvasTTY's own (CANVASTTY_GITHUB_TOKEN, anyCANVASTTY_*TOKEN/*KEY/*SECRET/*PASSWORD) never reach an agent card, whoever starts it.every profile. A CLI signed in by API key (Claude Code with
ANTHROPIC_API_KEY, Qwen Code withOPENAI_API_KEY,OpenCode, ...) keeps working.
shell, locale, terminal, temporary and XDG folders, Windows system folders, proxy and certificate variables,
EDITOR/VISUAL/PAGER, and the CLIs' home folders (CODEX_HOME,CLAUDE_CONFIG_DIR, ...). No providerkey, token, cloud credential,
SSH_AUTH_SOCKor OpenCode inline config.uses. Orchestrators cannot start plain terminal cards (unchanged; now tested).
env/secretEnv) is added on top, so a plugin that handsits agent a key keeps working.
CANVASTTY_AGENT_PASS_ENV=NAME[,NAME]in the environment CanvasTTY starts with keeps the listed variablesfor every agent card. It is read from the app's environment only, so an agent cannot widen it for itself.
ANTHROPIC_API_KEY) now needsCANVASTTY_AGENT_PASS_ENV=ANTHROPIC_API_KEYor a launch contribution'ssecretEnv.Sign-ins kept in the CLI's own files or the keychain are not affected. Documented in
docs/installing-and-security.md(en/ru/zh-CN) and in the delegation table ofdocs/agent-orchestration.md.person and can still read a key from their shell profile file, and a plain terminal card runs the login shell, which
reads it.
tests/agent-ambient-keys.test.mjs(fake values set on the test process): plain terminal; the person'sClaude, Codex, OpenCode and Qwen cards keep ordinary keys but get no admin or CanvasTTY credential, also under a
recording isolation layer; subagents with and without a layer; a plugin-started plain terminal; an orchestrator
cannot start a plain terminal; the environment actually handed to the PTY of OpenCode, Claude, Codex and Qwen
subagents built through the real runtime and browser bridges holds none of the fake values (and fails without the
fix); the opt-in; case-insensitive names (Windows).
tests/launch-contributors.test.mjs: a plugin-started agentdrops ambient keys but gets the key its contribution names in
secretEnv.Orchestration and decisions
3. Decision services could not tell an OpenCode write from an edit.
fix(opencode): send OpenCode's file tools to decisions in Claude Code's shapeswrite,editandmultieditas aneditwith only{file_path, content}. An edit's replaced text was missing, so a service could not see what itremoves.
Write {file_path, content},Edit {file_path, old_string, new_string, replace_all},MultiEdit {file_path, edits}. Bash andapply_patchare unchanged.
tests/decision-hooks.test.mjs: each shape end to end, and a MultiEdit outside the project is stilldenied.
4. An OpenCode subagent's shell prompt reached the orchestrator, not the person.
fix(opencode): bring a subagent's shell ask to the person, never to its orchestratorbashon "ask" (your rule, unchanged). In a live run,a subagent's
lsprompt sat unanswered.wait_for_agentreturned the same pending prompt on every call, so theorchestrator gave up. It could also have answered the prompt itself, because
send_to_agenttyped into theterminal whatever its status, and Enter answers OpenCode's prompt.
send_to_agentdid not checkneeds_approval, andwait_for_agentcould not wait for the person'sanswer.
send_to_agentis refused while the subagent isneeds_approval(not retryable). Nothing is written, and therefusal says to tell the person.
wait_for_agentreturns a given prompt once. A later wait on the same prompt waits for the person's answer andthe end of the turn.
bashis not allowed in any new case, and decision services answer as before.tests/opencode-subagent-ask.test.mjs. It runs the real OpenCode plugin module,RuntimeGateway,DecisionHooks,TerminalManager,AgentControlServiceandScopedOrchestrationHandler, with no model.5. An orchestrator could approve a Codex subagent's command through the control CLI.
fix(orchestration): an orchestrator's control connection never answers a subagent's approval promptchoose <id> --choice 1on a Codex subagent's"Would you like to run the following command?". That approved the command for the person.
dismissanswered it"No". The orchestrator skill even described these as menus
choose/dismisscan act on.choose/dismisschecked only that the menu was the one observed, not what it asks.sendwas refusedthere, but with a retryable
NOT_READYthat said "wait".needs_approval, or while Codex's own approval prompt is on screen (it can appear beforethe hook report), the orchestrator's
choose,dismissandsendgetNEEDS_PERSON. That refusal is notretryable and tells the orchestrator to tell the person.
interrupt, and theperson's own connection.
tests/orchestrator-approval-answers.test.mjs. It runs the real control CLI request path over a socketwith an orchestrator's grant: before the fix, the first
choosewrote the keys. It also checkssend_to_agentateach CLI's approval signal.
UI and language (6 and 7 from #149)
6. Parts of the UI ignored the UI scale.
fix(ui): scale the card menu, hints panel and plugin settings with the UI scaleProblem: at Settings → Appearance → UI scale 0.85 or 1.25, these stayed at 1.0:
To reproduce, set the scale to 1.25 and open each.
Cause: their rules used fixed
pxsizes instead of--ui-scale.Fix: the existing classes use
calc(... * var(--ui-scale, 1)). Nothing changes at 1.0.Test:
tests/ui-scale-chrome.test.mjs. Checked on Linux (xvfb) in ru/en and two palettes at 0.85/1.0/1.25;screenshots in fix(ui): follow the UI scale and the selected language in canvas chrome, Settings and plugin surfaces #149.
Area: UI.
7. Untranslated text in the Russian canvas hints and wheel settings; wrong key caps.
fix(i18n): finish the Russian canvas hints and format their key caps like the referencepartly English. Long labels in Settings → Keyboard shortcuts were cut off. The hints showed "Alt+ArrowUp", and the
shortcut reference showed ",nd+Comma" on macOS.
rustrings, no wrapping on those labels, and raw key names in the hints formatter.wheel/navigation settings' text (only Settings shows it) moves from the startup table to
canvasNavigationText.tsnext to Settings, so the fix makes the startup bundle smaller instead of larger.
tests/ru-shortcut-hints-i18n.test.mjs;tests/renderer-bundle-splitting.test.mjs(the settings text is not inthe startup bundle).
14. HOME's model-limit rows showed a bare "9%", which read as 9% left.
fix(ui): say what is used and what is left in HOME limit rowscountdown. "Used" was only in the hover / screen-reader text. Codex's own status line shows "% left", so 9% used read
as 9% left. Usage history's latest observation (the "Latest observation" value and the "No observations in this
period" note) also showed a bare percent. To reproduce, spend a little of a Codex or Claude window and open HOME.
usedPercentand Claudeutilizationare the used share.screen-reader line. Usage history uses one helper,
quotaShareText, for both of its texts. Two i18n keys(
limitUsedPercent,limitLeftPercent, ru/en) and three CSS rules that keep the countdown on the right and let theshare text shorten with an ellipsis in a narrow row. Nothing else on HOME changes.
tests/quota-used-left-labels.test.mjs: Codex and Claude fixture payloads keep 9% spent asusedPercent9; the real HOMELimitRow, rendered without Electron in ru and en, shows used and left in the row andin its hover line; usage history's templates have no bare percent left.
15. A plugin page whose native code or module was off only got "Plugin service is not running."
fix(plugins): say when a plugin's native code or module is off, with a way to the settingProblem: when a plugin page asked its own service and the person had not trusted the plugin's native code, or
had turned off the module that runs the service, the page and the host notice only said "Plugin service is not
running." (in English, also in the Russian UI). Nothing said why or where to change it. A trusted service that had
crashed gave the same text without its error. To reproduce, install a plugin with a service and a page (for example
examples/plugins/service-echo), leave its native code off and open the page.Cause:
PluginManager.assertServicechecked only that the service is in the active manifest, so every suchrequest reached the supervisor, which knows only "not running".
Fix:
(
src/shared/pluginServiceNotice.ts);Settings → Agents → Extension native code (or Settings → Plugins for a module). A notice with a button stays 8 s
instead of 2.6 s and takes clicks only while it has one; other notices are unchanged;
error.
The request still fails as before; nothing is trusted or turned on by the notice.
Test:
tests/plugin-service-notice.test.mjs: through the realPluginManagerandPluginServiceSupervisorwith the
service-echoexample, native code off and module off give their texts and settings sections, in ru anden; a crashed trusted service keeps the generic text with its last error and no settings notice; the toast shows the
button only when it has one; the frame, App and Settings are wired to it.
Area: plugins / UI.
GitHub sign-in (plugin showcase, from #149)
8. A started GitHub sign-in could not be cancelled; failures were silent or showed raw IPC errors.
fix(github): cancel device sign-in and report denied, expired and failed outcomesProblem:
top of Settings → Plugins, also in the Russian UI.
Cause: no cancel path. Every poll error was treated as retryable, terminal OAuth answers were ignored, and the
renderer showed the raw IPC error.
Fix:
cancelDeviceFlowover the existing IPC);deviceFlowStatereports idle, pending, denied, expired, failed or cancelled, with a localized status line in theSettings error style;
The new lines live in
PluginSettingsSection(the Settings chunk), not in the startup string table.Test:
tests/github-auth.test.mjs,tests/github-auth-integration.test.mjs,tests/github-signin-errors-ui.test.mjs.Area: sign-in.
9. A timed-out profile request left the sign-in "pending" forever.
fix(github): report a timed-out profile request as a failed sign-in/userrequest then timed out, the flow stopped silently. Itsstatus stayed "pending", and Cancel did nothing.
AbortErrorwas taken for a cancellation.tests/github-auth.test.mjs.10. A 429/5xx with an OAuth error body ended the sign-in after one answer.
fix(github): retry HTTP 429/5xx token answers that carry an OAuth error body{"error":"temporarily_unavailable"}ended the sign-in as an unknown error.
slow_downkeeps its own handling.tests/github-auth.test.mjs.Tests
11.
browser-engine-tabsfailed about 1 run in 10 under load.test: wait for the click's navigation in the engine-tabs click testnow and then on
mainwith "the click navigated: a new revision". It failed 6 of 50 and 9 of 100 runs under load.events, and the test's
ready()returned on the old document'sreadystate.h.ready(tabId, { after })also waits until the document revision has passedafter. The click testwaits for the navigation the click started and checks the new URL.
before the fix and 0 after.
12.
terminal-session-storefailed on every run from some checkout folders.test: ignore the checkout path in the session store's secret-word checkcheckout path contains "environment", "token", "buffer" or "capability" (for example
~/environment/CanvasTTY).cwdisprocess.cwd(), and the test scanned the whole file,cwdincluded, forthose words.
cwdvalue out. Everything else is checked as before.environment-check: it fails before the fix and passes after.Verification
Results on this branch (macOS arm64, throw-away HOME/XDG/CLI homes, head
93cc2bb4):npm run build(includes typecheck) andnpm run audit:secrets: pass-2,788, fixes 14 and 15's +4,922; fix 13 changes no renderer code
Conflicts with open PRs
orchestration-catalog.mjs, the generatedcatalog.json,docs/agent-orchestration.md, the orchestrator skill). The conflict is additive.has a doc-comment conflict in
opencode-decisions.mjs.docs/UI_CONTRACT.md. Keep feat(canvas): add Auto-sort to the empty-canvas Tools submenu #136's bullets and add fix 6's clause.conflicts except an additive one in fix(isolation, safety): keep CanvasTTY's private data out of agents' reach #161 (
ProviderRuntimeLaunch.preparetakesdecisionReads,accountHome, thenfix 13's
environment). Fixes 14 and 15 merge into them without conflicts.PluginFrame.tsxonly, at two adjacent importlines. Keep both imports (its
pluginLocalizedText, fix 15'spluginServiceNotice).Left for you to decide
pattern in
codexApprovalPromptwould make it the person's.decision services as the gate, say so; that variant exists. Fix 4 implements your rule as written.