Commit Graph
5438 Commits
Author SHA1 Message Date
Mohamed Boudra 09e8139f39 test(server): the test providers list two models with thinking options, and the live switches run
The fake providers listed one model and no thinking options, so once the daemon validated
selections against the catalog the live model and thinking switches had nothing valid to switch to
and skipped. Every fake provider now lists two models with thinking options, and the switches run
for all three providers without depending on installed binaries.
2026-11-05 23:25:43 +01:00
Mohamed Boudra 64146208bd test(server): live model and thinking switches use values the provider lists
The daemon rejects a model or thinking option its provider does not list, so the live-preferences
e2e can no longer switch to a made-up id. A provider that lists nothing to switch to skips the case.
2026-11-05 23:25:43 +01:00
Mohamed Boudra 3606a3c7a2 fix(plugins): a composer refreshes a stale catalog on resumed demand, shares its store watches, and is released with its last holder
Watching again, or reading once, after the catalog was invalidated or failed
while nothing watched now starts one deduplicated refresh; the observer
refetches on mount. Reconnect repair only refetches watched queries, so an
unwatched catalog stayed stale.

Each subscribe installed its own session and draft store callbacks, so N
watchers received N squared notifications, including for other agents' drafts.
One pair of subscriptions now serves all listeners, and the draft callback
ignores drafts of other agents.

A composer whose agent was removed while it was held was only released on the
next session change. The last holder letting go now checks too, and an archived
agent's idle composer is releasable.
2026-11-05 23:25:43 +01:00
Mohamed Boudra b1c6d8916c fix(plugins): useComposers holds the composer it names, and a composer lives as long as its agent
PluginComposersSource.subscribe watched the stores directly and never the
composer model, so a component using useComposers did not keep the model's
catalog observer alive and reconnect repair skipped it. subscribe now takes the
selector and holds the model it names, following focus.

Dropping a composer when it went idle left a command a plugin had saved calling
a dead model after the tab reopened. A composer is now dropped when its agent
leaves the session and nothing watches it or has one mounted.
2026-11-05 23:25:43 +01:00
Mohamed Boudra 8215535631 fix(plugins): a running agent's composer validates and refreshes against the agent's own catalog
setAgentModel and setAgentThinkingOption checked the provider's global catalog,
while the picker reads the catalog of the agent's directory, so a model only
the project lists was rejected. The validation input now carries the directory.

The composer read the catalog once with no observer, so reconnect repair, which
refetches only active queries, never refreshed it and a failed load never
retried. While anything watches, the composer now observes the catalog query.

The shared composer map kept every agent's composer for the session. It is now
dropped when nothing watches it and no composer is mounted.
2026-11-05 23:25:43 +01:00
Mohamed Boudra 58bb1c5667 test(plugins): a Command Center agent item reaches the composer through the real client state
The command-path test now reads workspace and agent through
createPluginClientStateSource over seeded session stores, so an empty
lookup, which makes the Command Center return before calling the item,
would fail it. Also records at the focused-agent publisher that it
publishes in an effect.
2026-11-05 23:25:42 +01:00
Mohamed Boudra ded20eb85c fix(plugins): composers by reference follow the focused agent and reach retained composers
- `{ focused: true }` is the agent Command Center hands to agent items, published by its
  registration through plugins/focused-agent.ts and empty outside a workspace. The composer source
  reads that module instead of the remembered workspace selection, the navigation store and the
  layout store, which also removes the require cycles those imports added.
- A composer the app retains in a workspace the user left has no live input. `insertText` and
  `setPrompt` now store the text with `replaceDraftText`, which asks the mounted input to show it;
  before, the text reached the store but the retained input never displayed it.
- `model` and `thinking` read exactly what the picker resolves before the catalog loads (no
  models), then re-resolve when it lands, instead of reading null.
- The daemon validates a thinking option against the model the agent runs: the runtime model when
  the provider lists it, else the configured one.
- The composer sources subscribe to query-cache updates only. A query another component creates
  during its render is announced as `added`, and a subscriber that updates then made React warn
  about updating a component while rendering a different one. A catalog read no longer starts its
  load synchronously either.
2026-11-05 23:25:42 +01:00
Mohamed Boudra f665cc697c feat(plugins): reach a running agent's composer by reference
useComposers(selector) in any plugin component and composers(selector) on every command context return the composer of a running agent, named by id or by focus, whether or not its tab is open.

A running agent now has one store-backed composer model apart from any mounted tab. The mounted composer plugs its cursor-aware insert in while mounted; otherwise insertText and setPrompt write the agent's saved prompt. model and thinking resolve through the provider catalog with the picker's own function, so a read equals what the picker shows.

The host reads one ComposerModel from context and no longer knows whether it is in an agent, a draft or New workspace. The daemon rejects an unlisted model or thinking option for a running agent, replacing the app-side guard.
2026-11-05 23:25:42 +01:00
Mohamed Boudra 03580ce50b fix(plugins): a New workspace item's commands stop working once its project or host is left
Every item received the same command functions, which act on the current screen state. A lookup
that finished after the user switched projects could still call setBranch or setMetadata and
overwrite the new project's choices; the reset on a switch only clears what was already written.

The setup model now hands out commands per target with commandsFor({ serverId, projectId }).
They reject with a "moved to another project or host" error when the screen is on another target,
like the lock rejects, so the promise only resolves when the change took effect.
2026-11-05 22:20:09 +01:00
Mohamed Boudra ee01397101 fix(plugins): New workspace items remount when the project or host changes
An item stayed mounted across a project switch, and its Combobox kept the previous project's
results (same query key, cached data) selectable until the next search returned, so an issue from
the old project could set the branch and metadata on the new one. A Combobox cannot detect that
its inline search now means another project, so the item host keys the items by host and project
id. Items and their search results start fresh for the new target, and a mount effect sets up
again after the model clears the branch and metadata.
2026-11-05 22:20:09 +01:00
Mohamed Boudra d484a841ed fix(server): a branch the caller named is not renamed from the first agent context
The daemon marked every new branch-off worktree for automatic naming, recording the chosen branch
as the placeholder, so the first agent's prompt could rename a branch a plugin or client had
named through branchName. Only a branch the daemon derived from the worktree slug is a
placeholder now.
2026-11-05 22:20:09 +01:00
Mohamed Boudra cd787881d9 fix(plugins): a host switch drops the branch and metadata even when the project id repeats
A remote repository keeps the same project id on every host, so comparing the project id alone
missed a switch from host A to host B for the same repository. The target is now the host and the
project id together.
2026-11-05 22:20:09 +01:00
Mohamed Boudra 8d260ddcb6 test(plugins): the project-switch spec expects the other project's metadata to be empty 2026-11-05 22:20:09 +01:00
Mohamed Boudra 08aaaae15b fix(plugins): a branch and metadata chosen for one project are not sent for another
The setup model outlives the project and host it was chosen for, so an issue picked for project A
was sent on workspace.create after the user switched to project B. The model now drops the branch
and metadata when the project changes (project ids are host-scoped, so a host switch counts). A
project that is still resolving is not a switch. The base selection is left alone: the built-in
picker keeps a branch pick across a project switch, and that behavior is unchanged.
2026-11-05 22:20:09 +01:00
Mohamed Boudra 45cee0b613 fix(plugins): a Combobox shows a failed refresh instead of the old results
The query keeps its cached data when a reopened list fails to refresh, so the old items stayed
selectable and the error never showed. A failed search now clears the list so its error shows.
The migration guide maps addNewWorkspaceItem, which its registration-coverage test requires, and
the superseded-search test waits on a later search instead of sleeping.
2026-11-05 22:20:09 +01:00
Mohamed Boudra b376a34cc4 fix(plugins): New workspace commands have the same effects as a pick, even from a mount effect
The screen attached the pick side effects (remembering the isolation, syncing the picker's PR
attachment) as listeners in its own useEffect, which runs after a child item's mount effect. A
mount-time setIsolation("worktree") switched the screen but was not saved, and setBaseRef skipped
the attachment sync. The effects are now injected when the setup model is built, so a command and
a pick run the same code from the first render. Closing the picker stays in the picker's handler.
2026-11-05 22:20:09 +01:00
Mohamed Boudra 97284bea8a fix(plugins): New workspace commands work from mount effects, and branch and base ref match what creation uses
A command called from a New workspace item's mount effect rejected because the
setup model only received its inputs and the pickers' handlers from a passive
effect on the screen, which runs after a child's. The model now takes its
inputs at construction, picks the base and isolation itself, and is refreshed
in a layout effect; the screen supplies the side effects of a pick (the PR
attachment, the remembered isolation) as listeners.

The branch a plugin sets was dropped when no base was resolved, such as on a
detached HEAD, because it was sent only for an explicit branch-off. It now
applies whenever the worktree cuts a new branch, decided in one place that
mirrors the daemon.

baseRef.name was the display-shortened branch name, so setting back what was
read based the worktree on the local branch instead of the origin one. It is
now the exact ref creation uses, and host and baseRef are frozen like project
and metadata.
2026-11-05 22:20:08 +01:00
Mohamed Boudra 5a17ff1fd3 feat(plugins): New workspace items set the branch and workspace metadata
addNewWorkspaceItem mounts a plugin field on the New workspace screen, and
useNewWorkspace() exposes host, project, isolation, base ref, branch and
metadata with commands that follow the composer item rules: they act on the
screen's state when they run, resolve once applied, reject while the screen
is locked, and take JSON payloads. Host and project are read-only because a
command that switched them would unmount the item that called it.

The screen's pickable state (base selection, manual isolation, branch,
metadata) moves into one plain store so pickers and plugin commands write to
the same place; the screen reports derived facts to it on each render. The
branch is sent as source.branchName only for a branch-off worktree, and
metadata on workspace.create.

Items render through ComposerControl with a new "field" surface (badge on
desktop, full-width field when compact). Combobox ships here: Paseo owns the
loading and error states, keeps previous results, and drops superseded
searches.
2026-11-05 22:20:08 +01:00
Mohamed Boudra ba207ff5ef test(app): composer item test installation carries the subscribe main added to InstalledPlugin
Main's plugin streaming (#8527) made subscribe a required field of InstalledPlugin; the
composer item model test builds its own installation and predates it.
2026-11-05 21:55:10 +01:00
Mohamed Boudra c4d5915732 fix(plugins): a built-in select in the compact sheet keeps its action name
The shared control named every icon-only select by its value ("Speed: Fast"),
including in the compact sheet, where the row already shows the value. The sheet
row is named by its action again ("Select speed"), as before the shared renderer.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 46f3ac94a3 fix(plugins): attachment commands honor the composer lock
The creation request captures the attachments when it starts, so an attachment a plugin added or
removed afterwards was lost or left in the request. They refuse like the prompt commands.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 4d5b761bc8 fix(plugins): a kept command honors the composer lock, and useAgent metadata is frozen
A plugin action that awaits an RPC holds the composer it was created with. The lock was captured
at that render, so a submit that started meanwhile did not stop its insertText. Commands now ask
the composer whether it is locked when they run.

The agent snapshot a plugin gets from useAgent froze only the outer metadata record. It now uses
the same deep frozen copy as the composer.
2026-11-05 21:55:10 +01:00
Mohamed Boudra f9fde693cf fix(plugins): draft metadata survives a remount, plugin commands follow the composer lock
Draft metadata was component state while the prompt lived in the draft store, so a composer that
remounted across the compact breakpoint kept its text and lost the plugin's choices. It now lives
in the draft store beside the prompt, is cleared when the draft is sent, and is not cleared when
the prompt is emptied. It is not persisted across app restarts.

While the New workspace composer is creating the agent, the built-in controls are disabled but
plugin controls and commands were not. They read the composer's own lock: Paseo controls render
disabled, and setPrompt, insertText and the agent commands refuse.

The options and metadata a plugin reads are frozen copies, through the same helper the timeline
projection uses. The composer control test mounted components in JSDOM, which the testing rules
ban; it now runs in the browser project.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 173a1197db fix(plugins): composer option resets and model checks match the documented commands
A draft kept `null` as an option value, so `setOptions({ key: null })` sent the null to the
provider and creation failed. A null edit now removes the key from every draft layer, as the patch
does on a running agent. A running agent's `setModel` accepted ids its catalog does not list; it
now rejects them the way a draft does.

The composer controls imported the toast host through the toast context, which pulled the host's
animation module into every module that loads plugins. They use the API-only context instead.
The migration guide's registration table gets the `addComposerItem` row its test requires.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 9e528c5776 test(plugins): lock draft option ownership without a subscription, name the overflow-sheet steps
The draft option test subscribed the options model to the form, which in the
app happens in a render effect, so it passed without the model observing the
form's provider itself. A case with no subscription now fails when that
observation is removed.

The overflow-sheet spec reads as steps through a helper.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 3685a08c43 test(plugins): composer commands tested on the models, overflow sheet in Playwright
The draft composer's command rules (unavailable provider, unlisted model,
edits belonging to the provider the form has now) moved from the React hooks
into the form store and the draft options model. They take the host's catalog
as a plain input, so the command tests run on the real models with no module
mocks and no renderer.

The overflow-sheet test faked the portal it was meant to verify. It is
replaced by a Playwright spec that installs the composer-controls example and
checks its rows in the narrow composer's Features sheet; it fails when the
sheet's context bridge is removed.
2026-11-05 21:55:10 +01:00
Mohamed Boudra d165b07682 fix(plugins): composer commands run on current state, report failures, and reach the overflow sheet
Draft commands read the state captured at render, so a plugin's
`await setProvider(); await setModel()` acted on a stale provider. The draft
form and options are now plain stores that commands read live, and
`setProvider`/`setModel` reject an unavailable provider or a model the loaded
catalog does not list. Draft `options` include the plugin's pending edits.

Shared composer controls own the pending state and show a rejected command as
an error toast, like the built-in controls. Callback payloads are objects
(`{ value }`, `onPress(event)`) so they can grow without breaking plugins.

The overflow sheet's content portals to the app root and lost the composer's
agent, input, toast and query contexts, so plugin items drew nothing in it.
`useComposerSheetBridge` carries them across.
2026-11-05 21:55:10 +01:00
Mohamed Boudra 4e96928c64 feat(plugins): composer items and controls in all three composers
A plugin registers a component with addComposerItem and reads and changes the
composer through useComposer(): provider, model, mode, thinking, options,
prompt, attachments and agent metadata. The agent, draft and New workspace
composers provide the same ComposerInput and ComposerAgent halves, so a
command reaches the right model without the plugin knowing which composer
it sits in.

Toggle, Select and Action are drawn by one ComposerControl renderer that
the thinking select and provider option controls now use too. Controls sit in
the toolbar when there is room and become rows in the existing overflow sheet
otherwise, so on a phone they appear in the overflow. Plugin items count in
the density presence so the composer condenses to make room. Model and mode
stay specialized but share the trigger, density and overflow.

Draft metadata is kept locally, sent at creation and reset when the prompt is
sent. PluginAgentSnapshot.metadata, deferred from the metadata phase, lands
here. The unused provider badge is removed.

vitest resolves @floating-ui/react-native to its ESM build: the CJS entry
requires the real react-native, whose Flow sources do not parse.
2026-11-05 21:55:10 +01:00
Mohamed Boudra c22b97506f docs(server): the handle-less recreate gap spans the agent.create.before hook 2026-11-05 21:25:10 +01:00
Mohamed Boudra 4216cdc7b5 docs(server): name the metadata gap when a handle-less agent is recreated on load 2026-11-05 21:25:10 +01:00
Mohamed Boudra e07440ae06 fix(server): reopening a closed agent keeps metadata saved just before
The loader reads the agent record, then queues a resume. A setMetadata
queued ahead of that resume saves a newer record, and the open hooks saw
it, but registration took metadata from the loader's older read and wrote
it back over the saved patch. Registration now uses the metadata read
inside the lifecycle lane.
2026-11-05 21:25:10 +01:00
Mohamed Boudra 4713ba8819 docs(plugins): the hook result comment names metadata as the field that stays 2026-11-05 21:25:10 +01:00
Mohamed Boudra 762f5f023e test(server): fixtures for agents and workspaces carry metadata
Agent snapshots and workspace records now always carry metadata, so the
hand-built fixtures and expectations in three existing suites name it.
2026-11-05 21:25:10 +01:00
Mohamed Boudra 50e830ac01 fix(plugins): omitted metadata in a workspace.create.before result means unchanged
Same handoff as agent.create.before: a legacy hook that returned only
{ source, ... } dropped the caller's metadata and anything an earlier
hook added, and workspace provisioning then defaulted it to {}. The
omitted field is carried forward at each handoff; an explicit {} still
replaces.
2026-11-05 21:25:10 +01:00
Mohamed Boudra f2f8a84041 fix(plugins): omitted metadata in a hook result means unchanged
An agent.create.before hook that returned its own { config, env } dropped
metadata that an earlier hook in the chain had added, because only the
caller's original metadata was restored at the end. The omitted field is
now carried forward at each hook handoff, as agent.open.before already
does.

The agent.open.before result type also required metadata, so an older
TypeScript plugin that returned its own object no longer compiled even
though the daemon accepted it. The result type now makes it optional.
2026-11-05 21:25:10 +01:00
Mohamed Boudra 7ddaf59aa7 fix(plugins): an agent.open.before result may omit metadata
The result schema required metadata, so a plugin written before metadata
existed that returns its own object instead of spreading the request
failed validation and the session open failed, even for agents with no
metadata. An omitted metadata now means unchanged and is filled back in
from the request so later plugins in the chain still see it; a present
value that differs is still rejected.

The restart test now sets metadata through the PaseoApi handles and
checks that agent.open.before sees the patched value after the restart.
2026-11-05 21:25:10 +01:00
Mohamed Boudra 35e136314d feat(plugins): agents and workspaces carry plugin metadata
Plugins had nowhere to keep their own data on an agent or workspace, so
Phase 5 and 6 surfaces would each have invented a side store. Both now
hold an opaque Record<string, JsonValue> that Paseo stores, snapshots and
hands to hooks without reading a key.

Set at creation or changed with agent.metadata.update.request /
workspace.metadata.update.request (top-level keys replace, null removes).
Persisted on the agent and workspace records, so it survives a restart.
Reaches agent.create.before (which may add to it), agent.open.before
(read-only), agent.create.after and workspace.create.before/after.

Gated once on server_info.features.entityMetadata; the client throws
"Update the host" instead of sending to a host without it.
2026-11-05 21:25:10 +01:00
Mohamed Boudra f4ddcf7a5b test(server): an apply to a plugin that reports controls sends its changes as provider options 2026-11-05 20:25:11 +01:00
Mohamed Boudra 46b0466f3b fix(server): an apply never reads a requested option as one the plugin dropped
A plugin may run an option it never reports. Only a key it reported before can be
reported as removed, and a rewind reopens with the accepted request plus what the
plugin reported back.
2026-11-05 20:25:11 +01:00
Mohamed Boudra 429cf715ca fix(server): an apply reports the options the plugin reset along the way
Applying and unsolicited reports now share one conversion from the plugin's
report to a patch, so a key the plugin stops reporting is a removal on both.
2026-11-05 20:25:11 +01:00
Mohamed Boudra 4196fae928 fix(server): a plugin that stops reporting an option has reset it
The adapter only visited keys present in a new report, so a key the plugin
dropped to return to its default kept its stored value and its rewind value.
A previously reported key that disappears is now reported as removed.
2026-11-05 20:25:11 +01:00
Mohamed Boudra bbd93f76ee fix(server): a rewound plugin session reopens with its options as they stand
The reopen merged the plugin's reported options over the launch record, so a
key the plugin reset by leaving it out came back at its launch value. The
session now keeps the committed options, removals included. The provider-direct
example also refuses a combined request before changing anything.
2026-11-05 20:25:11 +01:00
Mohamed Boudra 9e70f5665a fix(server): a plugin decides which options it applies live
A plugin reporting `controls` and `providerOptions` receives every key a live
change touches, with or without a control, and applies them or rejects the
whole request. The daemon's live-key guard now runs only for a plugin still
reporting `settings`, which cannot refuse a key.
2026-11-05 20:25:11 +01:00
Mohamed Boudra f4210f39aa fix(server): a plugin provider's reported options are stored like any provider's
A control's `defaultValue` only says what the control shows for a key with no
value. Opening and applying store every option the session reports, defaults
and values nobody set included, as built-in providers do; the special case that
left a value at the default unset is removed.
2026-11-05 20:25:11 +01:00
Mohamed Boudra 448f258583 feat(plugin): provider plugins declare option controls and receive option changes
A provider plugin reports `controls` and the `providerOptions` it runs in
`session.config`, and receives running-session changes as
`session.configure` `changes.providerOptions` (null returns a key to its
default), under the same apply-or-throw contract as built-in providers.
A control may declare a `defaultValue`; a value a plugin reports at that
default for a key nobody set stays unset instead of being pinned to the agent.

A plugin that still reports `settings` keeps working: the daemon reads each
setting as a control plus an option and sends changes in both shapes
(COMPAT(plugin-provider-settings)).

The app resolves a control's icon as a Lucide name when it is not one of the
built-in kebab-case names, so a plugin's icon shows live and in drafts.
Public docs and the provider-direct example use only `providerOptions` and
controls; the migration doc gains the rows.
2026-11-05 20:25:11 +01:00
Mohamed Boudra 152a5ae731 fix(client): applying a config with a reset option on an old daemon writes the default it can take
applyAgentConfig({ options: { service_tier: null } }) reached an old
daemon as featureValues with null, which its Codex setter rejects. The
config now takes the same translation as setAgentOptions: a toggle goes
off, a choice goes to its default, and an option with no named default
fails with the host-update message before anything is sent.
2026-11-05 19:31:56 +01:00
Mohamed Boudra 9f39663722 fix(client): resetting an option on an old daemon writes the default it can take
setAgentOptions({ service_tier: null }) sent a feature write with value
null, which the old Codex setter rejects, leaving the paid Fast tier on.
A toggle now resets to off and a choice to its default; an option with
no control to name a default fails with the host-update message before
anything is written.
2026-11-05 19:28:40 +01:00
Mohamed Boudra 568bd447e3 fix(app): a profile stops saving a toggle the model in use does not offer
Switching a profile from Opus to Haiku kept the Fast edit, so the saved
profile carried fast_mode that no control draws and an old daemon's
provider rejects at create. The profile editor now drops a key an earlier
answer drew as a control and the latest one does not, the rule the draft
already follows; options no control ever drew are never touched.
2026-11-05 15:30:11 +01:00
Mohamed Boudra 41a36fa593 test(server): the MCP test agent manager resolves effective options 2026-11-05 15:30:11 +01:00
Mohamed Boudra f91ec81f7c fix(app): a draft keeps sending an option no control has drawn
The previous change stopped sending any edit the latest answer drew no control
for, which also dropped options set without a control (a plugin setting a
token budget on the draft). Only an edit of a key an earlier answer for this
draft drew as a control stops applying when a later answer draws none; every
other edit always applies.
2026-11-05 15:30:11 +01:00