Skip to content

Release 1.7.0: integrate pixel themes, reliability fixes, and desktop branding - #101

Merged
howdeploy merged 14 commits into
mainfrom
release/1.7.0
Sep 29, 2026
Merged

howdeploy merged 14 commits into
mainfrom
release/1.7.0

Conversation

@howdeploy

@howdeploy howdeploy commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

CanvasTTY 1.7.0

This integrates the reviewed pixel-theme and reliability work into one release branch, including the local fixes found while using the combined build. The application version, package lock, changelogs, and release target are now 1.7.0.

Included work

The 1.7.0 changelogs also collect the features already merged since 1.5.2: orchestration and additional providers, plugin services and launch environments, session restore, terminal and canvas navigation, performance improvements, and security hardening.

Credits

Thank you @teo-nex and @BIackFIame for the upstream PRs integrated here. The integration fixes and release preparation build on your work and retain the original commits.

Thanks as well to @SaneSanders, @cododel, @qSHEMq, @tab11pm, @kootik, and @howdeploy for contributions and integration work included in the 1.7.0 release. The history also credits commits authored as s079891, without a linked GitHub account.

Validation

Local checks passed: npm run build (including TypeScript checks), npm run audit:secrets, and all 17 tests in the affected settings/summary suites.

CI passed on commit 4c7333e: the full test suite, Even G2 tests, TypeScript, production build, browser/MCP smoke checks, Windows pipe-host checks and packaging, and macOS packaging/CLI smoke checks.

After merge, tag the merge commit as v1.7.0 and publish the Linux, Windows, and macOS installers and updater manifests through the release workflow.

BIackFIame and others added 14 commits September 29, 2026 05:51
…e settled

Startup could fail with "ERR_ABORTED (-3) loading 'data:text/html...'"
when the machine was busy. Since services start while the startup page
loads, the application surface could start its navigation while the
page was still committing or loading in its own renderer process. The
application surface then committed first, and the page's ERR_ABORTED
arrived afterwards, while the loadFile promise was still waiting.
Electron's loadURL/loadFile promise takes the first main-frame
did-fail-load it sees as its own, so the application load rejected with
the startup page's abort and startup showed the failure page.

Services still start while the page loads. The application surface now
waits until the page load has settled, which it usually has by the time
services are up. A close during the page load is still a quiet quit, and
a real page error on a live window still fails startup.

Measured with 40 sequential hidden launches per build, each helper
process (renderer, GPU, utility) paused at random for 20 to 300 ms
during the first navigations: origin/main failed 4 and 6 of 40, the
same loop with this change 0 of 80. The build before
"perf(startup): start services while the startup page loads" had 0 of
40. Without pauses all builds start 40 of 40; the median time to a
ready window goes from 342 to 379 ms.
Pasting a MiniMax API key into Settings -> Agents -> Provider API keys
turned the window black. The key field's onChange read
event.currentTarget.value inside the setDrafts((current) => ...) updater.
React runs that updater later, during render, whenever an earlier update
of the component is still pending (a paste over a character already in
the field, the second keystroke of fast typing). By then the event has
finished dispatching and currentTarget is null, so the render threw
"Cannot read properties of null (reading 'value')" and React unmounted
the whole root. The renderer process stayed alive with an empty #root.
Key length and shape are not the cause: a 120-character key pasted over
a pending edit breaks the same way.

The handler now reads the value while the event dispatches and passes
it to the updater. No other renderer updater reads an event (checked
with an AST scan over src/renderer).

Tests: tests/provider-secrets-settings.test.mjs bundles the real
component against a React stand-in whose updaters run after dispatch
and pastes fake sk-cp-/sk-api- keys of 120 to 10 000 characters, with a
trailing newline and space, over a pending edit (fails before this
change with the same TypeError); a line-level guard keeps event reads
out of state updaters in the renderer.
An error thrown while React renders unmounts the whole root, but the
renderer process lives on, so render-process-gone never fires and the
main process's crash reload never runs: the window stays black until
the app is restarted. That is what the broken provider key field did.

The React root now handles onUncaughtError. The first such error logs
it with its component stack and reloads the application surface in
place; sessions live in the main process and survive the reload, as
after a renderer crash. A second one within 30 s (or with no session
storage to remember the reload) shows a static page with a Reload
button instead of reloading in a loop.

Tests: tests/uncaught-error-recovery.test.mjs (reload, cooldown, clock
change, no storage, root wiring). Checked in a hidden app: an injected
render error reloads to a usable Settings screen, a second one within
the cooldown shows the recovery page; forcefullyCrashRenderer still
reloads through render-process-gone.
…e settled

Startup could fail with "ERR_ABORTED (-3) loading 'data:text/html...'"
when the machine was busy. Since services start while the startup page
loads, the application surface could start its navigation while the
page was still committing or loading in its own renderer process. The
application surface then committed first, and the page's ERR_ABORTED
arrived afterwards, while the loadFile promise was still waiting.
Electron's loadURL/loadFile promise takes the first main-frame
did-fail-load it sees as its own, so the application load rejected with
the startup page's abort and startup showed the failure page.

Services still start while the page loads. The application surface now
waits until the page load has settled, which it usually has by the time
services are up. A close during the page load is still a quiet quit, and
a real page error on a live window still fails startup.

Measured with 40 sequential hidden launches per build, each helper
process (renderer, GPU, utility) paused at random for 20 to 300 ms
during the first navigations: origin/main failed 4 and 6 of 40, the
same loop with this change 0 of 80. The build before
"perf(startup): start services while the startup page loads" had 0 of
40. Without pauses all builds start 40 of 40; the median time to a
ready window goes from 342 to 379 ms.
Pasting a MiniMax API key into Settings -> Agents -> Provider API keys
turned the window black. The key field's onChange read
event.currentTarget.value inside the setDrafts((current) => ...) updater.
React runs that updater later, during render, whenever an earlier update
of the component is still pending (a paste over a character already in
the field, the second keystroke of fast typing). By then the event has
finished dispatching and currentTarget is null, so the render threw
"Cannot read properties of null (reading 'value')" and React unmounted
the whole root. The renderer process stayed alive with an empty #root.
Key length and shape are not the cause: a 120-character key pasted over
a pending edit breaks the same way.

The handler now reads the value while the event dispatches and passes
it to the updater. No other renderer updater reads an event (checked
with an AST scan over src/renderer).

Tests: tests/provider-secrets-settings.test.mjs bundles the real
component against a React stand-in whose updaters run after dispatch
and pastes fake sk-cp-/sk-api- keys of 120 to 10 000 characters, with a
trailing newline and space, over a pending edit (fails before this
change with the same TypeError); a line-level guard keeps event reads
out of state updaters in the renderer.
An error thrown while React renders unmounts the whole root, but the
renderer process lives on, so render-process-gone never fires and the
main process's crash reload never runs: the window stays black until
the app is restarted. That is what the broken provider key field did.

The React root now handles onUncaughtError. The first such error logs
it with its component stack and reloads the application surface in
place; sessions live in the main process and survive the reload, as
after a renderer crash. A second one within 30 s (or with no session
storage to remember the reload) shows a static page with a Reload
button instead of reloading in a loop.

Tests: tests/uncaught-error-recovery.test.mjs (reload, cooldown, clock
change, no storage, root wiring). Checked in a hidden app: an injected
render error reloads to a usable Settings screen, a second one within
the cooldown shows the recovery page; forcefullyCrashRenderer still
reloads through render-process-gone.
…ores and sockets

Base protection now treats CanvasTTY's private data as credentials. A shell
or file tool call that names the agent-control token or descriptor, the
gateways' connection records, the provider and plugin secret stores,
account homes, the GitHub sign-in or prepared launch runs (paths taken from
the app's own userData folder, passed in by the app), or the control and
runtime socket folders under the temporary folder, is refused whatever the
program: readers, copies, encoders, sqlite3, recursive walks of the app
folder, interpreter one-liners and heredocs, curl --unix-socket, nc -U,
socat and Python sockets. The model is told calmly that agents cannot
control CanvasTTY this way and to ask for an Orchestrator launch.

The project, the app's settings, other sockets, an agent's own account
home and the bundled control CLI are unaffected.
…hen close

An unauthenticated or malformed request to the control endpoint now gets a
stable INVALID_REQUEST whose message says only sessions CanvasTTY launched
as orchestrators may use it and how to get one, with no protocol details,
token names or paths. An HTTP request line (curl, a browser) gets a
minimal 403 with the same text. Either way the connection is closed right
after, instead of waiting for the idle timeout; the connection cap and the
one-request-per-connection rule are unchanged.

Tests cover a guessed NDJSON request, garbage and an HTTP request, and that
the token file is 0600 and its folder 0700 even under umask 0 and a
pre-existing loose folder.
…review/pr-98-20260929' into dev/test-prs-96-98-20260929
…20260929

# Conflicts:
#	src/main/services/agent-control/AgentControlGateway.ts
#	tests/agent-control.test.mjs
@BIackFIame

Copy link
Copy Markdown
Contributor

@howdeploy, could you hold 1.7.0 for a short while? While testing orchestration live on a combined build we found problems that make delegation unreliable, and fixes are in progress on top of #100:

  • Subagent working folder: an OpenCode subagent treated the project as an external directory and stopped at a permission prompt when the project path had non-ASCII characters and a space. This looks like a Unicode normalization mismatch (macOS stores names in NFD, the orchestrator passes NFC). The fix resolves cwd to the on-disk path before launch.
  • Choosing a model for a subagent: spawn_agent has no model parameter, so an orchestrator cannot start a subagent on the model the person asked for (it falls back to the CLI default).
  • Discovering providers and waiting: core has no way to list which providers can be spawned and no wait tool. An orchestrator guessed and started exploring the filesystem instead of delegating. We are adding list_providers and wait_for_agent, and listing the valid provider ids in spawn_agent.

These are not regressions from #100; they also exist on current main. If you prefer to ship 1.7.0 now, we will send them as a follow-up PR instead. Otherwise we expect to post them here within about a day, with tests and a live check.

@howdeploy
howdeploy merged commit 3fe6fed into main Sep 29, 2026
3 checks passed
@howdeploy

Copy link
Copy Markdown
Owner Author

@BIackFIame Thanks for the detailed report and for working on the fixes. We'll go ahead with the 1.7.0 release and address these issues in follow-up patches, using community testing of the actual packaged application to confirm and prioritize problems.

Please send the fixes as a follow-up PR when they're ready. We'll work through them with the community after release rather than hold 1.7.0 for this batch.

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.

3 participants