A plugin session applied the settings it knew and silently stored the rest; it now rejects
a patch that names a key the plugin does not report. Codex opening on a model without the
saved speed ran Normal while the stored options still said priority; opening now reports
the tier it runs, as a later model switch does.
A session with no applyOptions (Pi) now rejects a patch instead of storing a value that
fails on reopen. Codex applies only service_tier, plan_mode and fast_mode live, since the
approval policy and sandbox are fixed when the thread starts. Claude also rejects a launch
option change while its first query is starting, after the options have been copied.
Replacing a started query to apply the option would end its background shells and
tasks. Once the query exists only Fast can change; before the first prompt every option
still applies, because it is read when the query is built.
A stored option has to describe what runs. OpenCode, OMP and ACP now reject a live change
to a key they apply only when the agent opens; Claude applies such a change from the next
turn and rejects it while a turn is running. Opening reports every select or setting the
session came up with, including ones nobody set, so a native default (Cursor opening with
Fast on) is stored as option state like any other.
A live service_tier: fast is stored as the catalog's Fast tier id instead of
a value the control cannot show, and a model switch to a model without the
stored speed reports Normal so choosing a supporting model again does not
bring the paid tier back.
A write with options folds featureValues over those options. A write
without them patches the stored options, unless the profile moved to
another provider, which starts from no options.
Live patches go through the schema used at creation. A Claude model switch
that cannot keep Fast reports fast_mode as removed so the stored options
match the session.
A config_option_update that arrived before the write response was emitted
as an event on top of the result the manager stores. Notifications during
open or apply now fold into the open patch or apply result, and a rejected
apply reports what the agent moved since nothing is stored. Live options
are validated with the creation schema. Tests drive a stdio ACP agent.
The unchanged app's profile form only knows the controls, so a name-only
edit dropped options it never shows, such as sandbox_mode. Its
featureValues now apply as a patch over the options already stored for the
profile (null removes a key), and a write with no featureValues keeps them.
applyOptions returns the options the session ended up with and the session
exposes its open difference as openOptionsPatch, so the manager stores the
patch and the provider's answer in one step. Removal is an ordinary patch:
the provider receives the default the key falls back to, or null, and returns
what it kept. Controls no longer take a default from a stored legacy feature
value.
Providers used to write their own reports into the agent's options while a session was being
created, resumed or updated, which needed a growing set of reconciliation, baseline and
suppression rules to stay in step with the daemon.
The contract is now transactional. A provider opens or updates with the options it is given and
emits nothing while doing so; if the result differs it says so once, afterwards, as
`options_changed`. Live apply hands the provider only the keys the patch sets and the daemon stores
them onto the agent's current options when the provider accepts, so the last write wins. A change
the provider makes on its own is stored and forwarded, not undone. Removing an option drops the key
and sends nothing live. Controls carry no state; `options` is the only option state.
A legacy `agent.create.before` hook's `featureValues` edits are applied as a patch over the
returned options, so its deletions and replacements hold. The unreleased `aliases` field on option
choices is gone; a pre-v0.10 Codex Fast preference is reported once as the model's tier id.
Reverts the storage migration from a987b4a43. The Codex speed control now takes its default from
the session's current options with the same rule the runtime uses (an explicit service_tier wins;
fast_mode: true applies only while service_tier is absent). This holds on create, resume, live
apply and for config.json defaults, and stored options stay as set.
A record holding `fast_mode: true` without `service_tier` ran Fast but the speed control showed
Normal, and picking Normal then wrote "default". The Codex session now normalizes the old boolean
at construction and reports the new shape through its existing options_changed event.
- Codex: drop displayOptions; a select choice may carry aliases so a saved Fast preference selects the model's Fast tier from options + controls alone.
- OMP: removing fast_mode restores the runtime's starting setting.
- ACP/Cursor removal tests run through real session initialization.
- agent.config.apply of a non-control option reaches the provider and shows in the next snapshot (daemon e2e).
- Daemon config store persists legacy-featureValues profiles as options on the next profile save.
- Removing auto_accept (OpenCode, ACP) turns live approval off; ACP selects return to their session default.
- agent.create.before hook featureValues edits override options the create already set.
- Claude commits fast_mode only after the runtime accepts it.
- Codex snapshots show a saved Fast preference as the tier the model resolves it to (AgentSession.displayOptions).
- Profiles are folded into options in the daemon config store; the config API still answers with featureValues.
- Snapshot options and provider.controls.list carry all effective options.
- Test: Claude fast_mode survives a restart through stored providerOptions.
Providers emit option controls over top-level providerOptions keys; the daemon stores one
options record per agent, merges config.json defaults as effective values, and applies patches
live via AgentSession.applyOptions. Legacy featureValues (wire, stored records, profiles,
schedules, hooks, MCP) fold into providerOptions; snapshots still carry features built from
controls. Snapshot options expose only keys that have a control.
Add AgentOptionControl descriptors, snapshot options/controls, an options patch on
agent.config.apply, provider.controls.list RPC, and server_info.features.providerOptionControls.
featureValues stays accepted on the wire (COMPAT).
Reject new turns before accepting work into a known dead Pi runtime. Keep process liveness separate from explicit session cleanup so Reload can still release resources and resume the conversation.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(server): recognize persisted parents during child creation
Creation only needs the caller's identity and workspace. Read the existing stored record when the parent is unloaded after restart, rather than requiring its provider session to be opened first.
* test(server): run persisted caller regressions in integration CI
* test(server): retry fixture removal while Windows releases handles
* fix(server): keep archived parents ineligible for child creation
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* feat(server): add background agents
An agent can be created background in any workspace. It stays a durable,
addressable agent, but default discovery, workspace status and attention
leave it out. Effective background is agent.background ||
workspace.background.
- Protocol: `create_agent_request.background` now marks the agent itself
and stays the workspace intent for a workspace the request creates.
Snapshots carry `background`. The daemon advertises
`features.backgroundAgents`. Clients advertise the `background_agents`
capability.
- Daemon: the flag persists in the agent record, so restart and resume
keep it. AgentDirectory owns visibility. Inclusive reads and
subscriptions from sources without `background_agents` see only agents
in background workspaces, because older apps would otherwise open a tab
for each one.
- Inheritance: omitted on agent-scoped creation, children and new
workspaces take the caller agent's background.
- MCP: create_agent accepts `background`.
- SDK: placing a background agent in an existing workspace, or under a
caller, fails fast with "Update the host to use background agents." on
older hosts.
- CLI: `ls -a` and `ls --background` add a BACKGROUND column.
* feat(app): hide background agents from tabs, track and status
* docs(plugins): point hidden helpers at background agents
* fix: keep background agents hidden across cache reload and connected hosts
* fix: keep remote creation working on older hosts and inherit foreign parents' background
* feat(app): open plugin settings from a cog with a tab per screen
The plugin row shows a settings button next to its switch when the plugin
registers settings screens. The actions menu keeps Logs, Reload and Remove.
The settings page shows one tab per registered screen; a plugin with one
screen shows no tabs. Tabs swap screenId in place, so existing
plugins/[pluginId]/[screenId] links and openSettings keep working.
* fix(app): render settings tab icons inside the plugin's runtime
Component icons now mount under the installation provider, so plugin SDK
hooks such as useTranslation work. Each icon has its own error boundary;
an icon that throws leaves its tab with the label alone.
* feat(app): name only the plugin in its settings page header
The header shows the plugin's manifest name, or its id when it has none,
for single- and multi-screen plugins alike. The selected tab names the
screen. The Plugins page and the header share one plugin list query.
* feat(app): show plugin display names and a Settings tooltip on the cog
Plugin rows on Settings → Plugins show the manifest name, falling back to
the id; when a name is set, the id stays visible beneath it. The row's
controls, logs sheet, confirmations and feedback use the same name.
The cog is a ToolbarButton labeled Settings, with a matching tooltip.
Plugin composer pills with a menu or popover now show an upward chevron at
every width, so a pill that opens a surface reads differently from a one-tap
action pill. Built-in Tasks and Subagents pills are unchanged.
The transcript's bottom inset used to be reserved only while some pill
existed. Plugins that add pills when a turn ends and remove them when the next
starts moved the transcript every turn. The inset is now reserved whenever the
agent has an active composer.
* fix(claude): show thinking options for models from settings.json
Models appended from Claude's settings.json were pushed to the catalog
without thinkingOptions, so the composer had no effort selector for them.
Resolve them like profile and additionalModels entries: manifest options for
built-in IDs, the standard Claude effort levels otherwise.
* docs(claude): clarify effort selection for settings models
Describe explicit effort selection without promising how every client initializes an unsaved value.
Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
Co-authored-by: wangyahao <wangyahao@fxiaoke.com>
---------
Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: wangyahao <wangyahao@fxiaoke.com>
An execution adopted during session initialization emitted turn_started before the manager could subscribe. Deliver the current turn to a new subscriber so the manager owns the run and routes Stop to OpenCode.
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(android): correct inverted chat wheel direction
* test(android): run wheel regressions with current React Native
Provide the native-only test context with React Native’s event dispatcher for an absent JavaScript instance, retaining real scroll handling.
Co-authored-by: Seth Jeon <paseo@sethjeon.com>
---------
Co-authored-by: Seth Jeon <paseo@sethjeon.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Separate runtime release from explicit turn cancellation, drain close-time cleanup before retirement, and reject late query creation. Add regressions for closure, pending tools and permissions, and asynchronous launch.
Co-authored-by: babo-jeonguk <278435975+babo-jeonguk@users.noreply.github.com>
* feat(keyboard): support hardware keyboards on iPad
Register app shortcuts as iOS UIKeyCommands so a paired hardware keyboard
drives the same bindings as web and desktop. Overlays (menus, comboboxes,
modals, command center) get first refusal on bare keys, and a focused
terminal keeps the keys it needs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(keyboard): record iOS hardware key observation spike
With a UITextView focused on iPad Pro M4/iOS 18.6, window and controller observers received Cmd+B, Cmd+P and Escape. Escape also invoked UIKeyCommand when registered, so the stream must be the sole emitter. Use observation for shortcuts and commands only for text-control ownership. Simulator intercepted Cmd+K as HID184; input capture blocked synthetic typing entirely. Keep capture off and verify the toggle via a rebind during simulator QA.
* refactor(keyboard): give overlays one key registration API
Overlay ownership now follows one layer-ordered registry on every runtime. Web retains focus trapping and restoration through its adapter; native registers the same key handler without a DOM scope. Picker navigation and dismissal no longer require a parallel native stack.
Co-authored-by: jeroenfrenken <16974549+jeroenfrenken@users.noreply.github.com>
* feat(keyboard): route native hardware keys through the shared resolver
Observe each physical press once, including native modal windows, and derive focus from declared composer and terminal surfaces. Native bindings and rebinds now use the same resolver and overlay precedence as browser and Electron input.
Co-authored-by: jeroenfrenken <16974549+jeroenfrenken@users.noreply.github.com>
* feat(keyboard): show shortcuts when hardware keyboards are present
Keep routing independent of compact layout and detect hardware capability for hints and settings. Rebind capture uses the shared overlay interface on native too. Keep the filtered sidebar model available for keyboard navigation while its panel is hidden.
Co-authored-by: Les Stroud <public@lesstroud.com>
Co-authored-by: jeroenfrenken <16974549+jeroenfrenken@users.noreply.github.com>
* test(keyboard): allow shortcut hints in compact menu assertions
Compact menus now display hardware keyboard hints. Assert action labels, order and uniqueness without treating a trailing shortcut badge as part of the action label.
* fix(keyboard): retain an overlay's opening layer in state
Capture the registry's current top when an overlay opens and retain that layer through rerenders. React Compiler removed the previous useMemo cache after the registry getter moved across modules, allowing a parent modal to rise above its child and start an infinite focus loop.
Compile this hook in Vitest as Expo does. The new hook regression failed first with layer 220 instead of 20, then passed with explicit opening state. Focused unit and browser tests pass, and the previously failing onboarding E2E completes through the real Expo compiler. Typecheck, lint and format pass.
* refactor(keyboard): let overlays own native hardware input
Remove the TextInput key handlers that duplicate declared overlay arrows and Escape in Add Project and Command Center. Remove Add Project's hidden responder now that native window sources observe keys without it. Keep soft-keyboard submit editing behavior.
* fix(keyboard): close Add Project outside its state updater
Root Escape previously called the parent close action from a React state updater, producing a cross-component render warning when navigation was still queued. Compute the previous page in the event handler and close outside the updater.
The existing Escape/Back E2E now batches navigation and dismissal through the DOM key source. It failed first with the AddProjectFlowHost render warning, then passed after the correction. Typecheck, lint and format pass; native Escape checks pass on Android and iPad.
* fix(keyboard): preserve Android dead-key composition
* fix(keyboard): recognize focused native text inputs
* test(keyboard): replace JSDOM coverage with browser tests
* refactor(menu): spell out arrow navigation branches
* fix(composer): claim fullscreen Escape on native
* refactor(composer): one fullscreen Escape module for every platform
The shared registration lived in fullscreen-exit.shared.ts with two one-line
re-exports (fullscreen-exit.ts and fullscreen-exit.native.ts). Metro needs no
native override now that the Android back button is its own adapter, so the
implementation moves into fullscreen-exit.ts and the browser test imports the
module every platform resolves.
---------
Co-authored-by: Ubuntu <jeroenfrenken@icloud.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: jeroenfrenken <16974549+jeroenfrenken@users.noreply.github.com>
When the ACP agent process exited while idle, the session kept its dead
connection and accepted the next prompt. The ACP SDK logs the failed
write but never settles the prompt request, so the turn stayed running
forever with no error.
startTurn now refuses a connection whose signal has aborted, and a turn
whose transport closes before its response fails with "ACP agent
disconnected" (with the exit code or signal when known). A response
that was already delivered still completes the turn, and a crash that
the exit handler reports keeps its "exited unexpectedly" error.
Fixes#7070
* test(server): diagnose recurring Windows setup timeout
* test(server): stop pending setup before removing its fixture
A test deadline does not cancel background setup. Abort and join it before deleting its working directory, and cover teardown with a deliberately live setup child. Keep the existing subdirectory shell assertions and timeout.
* test(server): preserve setup readiness filesystem errors
---------
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(server): return 404 for unserved well-known paths
The daemon web UI middleware fell back to the SPA's index.html for every
unknown GET, including /.well-known/* URIs. MCP clients that run OAuth
discovery (RFC 9728 protected resource metadata, RFC 8414 authorization
server metadata) received a 200 text/html body, treated it as malformed
metadata and retried instead of concluding the server has no OAuth.
Well-known paths now serve a real file from the dist directory if one
exists and otherwise return a JSON 404. Other paths keep the SPA fallback.
* fix(server): verify metadata routing through the daemon
Retain the contributed well-known routing fix with named resolver options and a full daemon regression that fails when metadata requests receive app HTML.
Co-authored-by: HomicideZero <219062565+HomicideZero@users.noreply.github.com>
---------
Co-authored-by: HomicideZero <219062565+HomicideZero@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* fix(workspaces): keep merged pull requests visible
A remote update before merge can leave the checkout behind the PR head. Match terminal requests by branch for display and require an exact fresh head match before automatic archive.
* fix(forges): retain pull requests targeting a fork
A fork checkout can have its own pull request as well as one targeting its parent. Compare both candidates before choosing which request to display.
* fix(forges): rank closed drafts as terminal
GitHub retains the draft flag after closing a pull request. Selection must use the request state so a closed draft does not override an open or exact-head request.
Plugin screens can report unavailable while a successful catalog request
is still pending. Track the host catalog state so these screens show
progress until their contributions register.
Keep missing surfaces, failed requests, and offline hosts distinct from
loading. Retain existing installed surfaces during catalog refreshes.
Cover pending-to-ready rendering with a real browser regression and
catalog lifecycle tests.
Related to getpaseo/paseo#7088
* feat(app): link #123 issue and pull request references in Markdown
Agents cite issues and pull requests by number; reading one meant looking
up the URL by hand. Markdown in a workspace now links forge shorthand
against the workspace's origin remote: `#N`, GitLab and Gitea `!N`, and
cross-repository `owner/repo#N` on the same host.
Each forge's URL grammar declares which sigil maps to which path, so the
syntax stays per forge: GitHub has no `!`, and its `#` targets /issues/,
which GitHub redirects to the pull request. Recognition goes through the
Markdown engine's text link regex so code spans, fences and authored links
stay plain without a second Markdown parse while streaming. The reference
alternative sits before path recognition so `group/project#12` is not read
as a file path.
* fix(app): keep http:// for self-hosted forge web links
A self-hosted forge cloned over http:// serves its web UI over http on
the same origin. Forcing https made reference, blob and tree links fail
against an HTTP-only port. Cloud hosts and SSH remotes stay on https.
Bring in what v0.15.0 shipped: the 0.15.0 entry in CHANGELOG.md and
CHANGELOG.zh-CN.md exactly as tagged (replacing the 0.15.0-beta.1 series
entry), version 0.15.0 across package.json/package-lock.json, the F-Droid
changelogs 150001-150004.txt, and the matching Nix npm deps hash.
* fix(pi): restore complete active-branch chat history
Replay the saved transcript ancestry instead of the compacted model context so older messages retain their native rewind identities and Fork chat boundaries.
* refactor(pi): name runtime options and assert restored tool output
* fix(pi): accept native system messages with text blocks
The library's web renderer marked an image inline only when it was a
direct child of a paragraph with other content. A linked image sits
under a Link node, so badges such as a review bot's
`[](url) Confidence Score` header rendered as
100%-wide, 320px-tall block images in the PR panel and file preview.
Images nested in links (or emphasis) now count as inline when their
paragraph has other content; a paragraph made only of images or
image-only links still renders them as blocks. Inline images now take
the line height with automatic width instead of a line-height square,
so wide badges and photos keep their aspect ratio instead of squashing.
* fix(codex): retain multi-file patch diffs in timeline details
Combine each file's diff in canonical file-change items and legacy patch
text, keeping the legacy parsers and envelope unchanged. Cover both legacy
lifecycle events and the failed status.
* fix(codex): handle path-only and moved files in patch diffs
Multi-file assembly generated headers without patch text and discarded
move destinations when reducing object kind tags. Skip textless entries
and retain move_path for generated destination headers, keeping the
single-file mapping and existing truncation limit.
Add mapper regressions for both cases, demonstrated failing on 179c3f7f6
before the fix.
* fix(codex): keep later files in bounded patch previews
Share the existing diff detail budget across files so a large first diff cannot hide later changes in canonical or legacy patch events.
Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
* fix(codex): preserve patch preview labels and move destinations
Keep complete file and hunk labels within the preview budget and count files omitted for space. Select the primary file from rendered content and retain move destinations in path-keyed and legacy events.
* fix(codex): bound generated file additions without argument overflow
* fix(codex): retain move-only files in multi-file previews
Represent moves without content changes as rename metadata and remove the provider move notice before classifying diff contents.
* fix(codex): keep move normalization inside multi-file previews
Recognize the provider move notice only while assembling multi-file diffs so the existing single-file path continues to retain its destination.
* fix(codex): retain empty additions in multi-file previews
---------
Co-authored-by: Okan YILDIRIM <63511519+ragokan@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
* feat(app): add first-run setup modal
New users landed on an empty "Add a project" screen and never found
providers, Paseo tools or phone pairing, which all live in Settings. A
one-time setup modal now walks them through Appearance, Providers,
Projects, Orchestration and Phone. Every step renders the same component
Settings uses, so changing something in setup changes it in Settings too.
The modal opens only when the setup host is connected, its workspaces have
loaded and it has no projects. Existing users already have projects, so
they never see it after upgrading and no migration is needed. Finishing or
skipping writes `@paseo:onboarding` on this client. It uses its own key
because it records feature state, and e2e specs replace app settings
wholesale. Settings > General > "Show setup again" reopens the modal on
step 1. Native phones skip the modal for now.
Shared pieces moved out so setup and Settings render the same thing:
- ThemePicker is a tile grid and replaces the Settings theme dropdown.
- PaseoToolsCard is extracted from the host page.
- The skeleton pulse moves to components/ui. Provider rows show it while
the first snapshot loads instead of "Loading…".
- The pair-device direct-connection hint links to the Tailscale docs.
The e2e fixture marks setup completed by default so existing specs keep
their starting state. The desktop smoke and lifecycle scripts, which start
from a fresh daemon, mark it before they continue.
* feat(onboarding): suggest projects from agent history
The setup's Projects step lists the repos the user already ran agents in,
with busy recent ones preselected, so a new user adds their projects in one
click instead of browsing for folders. With rows checked, the footer's main
button adds them and only moves on once every one was added; a checked row
always means "this will be added".
The daemon resolves each session folder to its project root (main checkout
for worktrees, repo root for subfolders) because only it can read the git
state; the app asks once per provider so a slow or broken provider never
holds up the others and can be retried on its own. Hosts without the
projectSuggestions feature show only the Add project button.
Onboarding e2e hosts read an empty Claude history with other providers
off, so suggestions never depend on the machine running the tests.
* fix(onboarding): keep late project adds on their step and show Retry early
- A finished add only advances setup while the Projects step is still open.
- A failed provider shows its Retry as soon as any provider answers.
- The desktop lifecycle check no longer reloads the window: setup never opens
there because no host is connected.
* fix(onboarding): bind late project adds to their step visit and freeze rows while adding
- Each step visit has an id; a finished add only advances the visit that started it,
so leaving and returning to Projects, or reopening setup, ignores it.
- Suggestion rows are disabled while their batch is being added.
* test(app): drive theme selection through the tile picker
Settings → Appearance no longer has a theme dropdown. The Escape test opens the
syntax theme dropdown instead, and the Android system-bars check taps theme tiles.
* fix(server): return project suggestion roots in the form project.add stores
Git reports Windows repo roots with forward slashes, so an added suggestion kept
showing because its path never matched the new project's root.
* feat(app): two control sizes, orchestration switches and mobile connect choices in setup
Controls had four sizes picked per call site, so a button in a dialog, a
settings row and a setup step came out at different heights. Controls now
have two: md (default; 32px with a pointer, 44px in the compact layout so
phone controls are full touch targets) and sm (28px, for tight chrome and
nested settings rows). The breakpoint picks md's height, not the caller.
Setup:
- Orchestration is three switches (Paseo tools, agent skills, CLI), all on,
applied on Continue. Turning one off undoes it; the desktop CLI uninstall
removes only the file this app installed.
- The Mobile step has store buttons and a "Choose how to connect" section:
Relay opens the existing pairing modal, Tailscale opens the docs.
Settings section docs links are one pattern, SettingsSection's `link` prop,
and modals sit on surface0 like a screen.
The vitest unistyles stub now resolves breakpoint values by window width,
as Unistyles does at runtime; without it md controls had no height in
browser tests.
* fix(app): finish setup on its last step and confirm undoing orchestration
In the compact layout Orchestration is the last step, and Finish closed
setup without applying its switches. The last step's work now runs before
Finish, the same as Continue.
Turning off skills now asks first, as Settings does. Removing the CLI keeps
a paseo command this app didn't install and says so. A failed Paseo tools
update shows a toast. Adding suggested projects no longer advances when a
row was checked while the add ran.
Chat sender attribution and the message preview toggle go back to the sm
button: md padding is a breakpoint value, and on web its media-query rule
beat the zero right padding that aligns the sender with its bubble.
* fix(app): ignore a late Finish once the user left the last setup step
Finish called complete(), which ignored the step visit, so pressing Back
while Orchestration applied its choices still closed setup and recorded it
as done. next(visit) now finishes setup from the last step, so a late
Finish is dropped the same way a late Continue already was.
* fix(app): hold the skills switch while setup applies its choices
The Paseo tools and CLI switches were disabled while Continue or Finish
applied them, but the skills switch only locked while a skills operation
ran. With skills needing no change, it stayed editable during a pending
tools update and the new value was silently dropped.
* fix(app): read setup's skills choice as off when no skill is picked
Saving an empty selection in the skills picker left setup's skills switch
on. Continue then asked the host to install nothing, the host reported
skills as not installed, and the step stayed put with no error. Setup now
reads skills as off with nothing picked, as Settings does.