Skip to content

[pull] main from microsoft:main - #1603

Merged
pull[bot] merged 42 commits into
code:mainfrom
microsoft:main
Aug 18, 2026
Merged

[pull] main from microsoft:main#1603
pull[bot] merged 42 commits into
code:mainfrom
microsoft:main

Conversation

@pull

@pull pull Bot commented Aug 18, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Copilot AI and others added 30 commits August 17, 2026 22:25
* Initial plan

* Show send spinner immediately on omni chat submit

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

* Fix routed chat submit feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>
Co-authored-by: meganrogge <merogge@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mirror Claude, Codex, and BYOK enablement through the experiment-aware renderer configuration layer instead of launch-time environment variables. Gate BYOK model publication dynamically and register providers when root configuration enables them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Browser: CDP proxy correctness fixes

* feedback
* Initial plan

* Fix omni chat opening duplicate window when adding context

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>
Co-authored-by: vritant24 <13074644+vritant24@users.noreply.github.com>
TunnelAgentHostContribution depends on ITunnelHostService, which was only registered in the
electron-browser layer. On web the contribution failed to construct with "depends on UNKNOWN
service tunnelHostService", so tunnel discovery never started and the Agents window always showed
the "Connect a host to get started" empty state, even with an online tunnel.

- Adds WebTunnelHostService, an inert ITunnelHostService for the web target. Hosting a tunnel
  spawns the VS Code CLI, which a browser cannot do, so the service reports that it never shares.
  isTunnelHosted() then keeps every discovered tunnel visible in the picker.
- Registers the service in sessions.web.main.ts before the contribution that consumes it.
- Removes the isWeb workaround in remoteAgentHostActions.ts that existed only to avoid this gap.
- Adds a regression test and records the cross-target requirement in the provider specification.

(Commit message generated by Copilot)
* chat: add experiment hook to test Luna for dictation LLM cleanup

Adds a 'dictationLlmCleanupModel' assignment treatment so the LLM
dictation cleanup model can be flighted. Control keeps the existing
copilot-utility-small selector (gpt-4o-mini); the treatment value
gpt-5.6-luna selects Luna. The lookup shares the existing cleanup
deadline and preserves the raw-transcript fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* copilot: publish hidden gpt-5.6-luna so dictation cleanup experiment can resolve it

The copilot vendor only publishes models to the workbench language-model
list when they are shown in the model picker (or are gpt-4o-mini). Luna is
not a picker model, so selectLanguageModels({ id: 'gpt-5.6-luna' }) would
return nothing and the dictation cleanup experiment would silently fall
back to the raw transcript. Add gpt-5.6-luna to the always-published
utility models so the treatment can resolve it while keeping it hidden
from the picker.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chat: harden dictation cleanup model experiment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: b8fa5c4e-601e-4267-87f4-366139bc420c

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b8fa5c4e-601e-4267-87f4-366139bc420c
Co-authored-by: vritant24 <13074644+vritant24@users.noreply.github.com>
…yling (#331337)

* Initial plan

* Size omni Add Context (+) button to match send button

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

* Match omni chat input editor background to window surface

The floating chat input window is hosted under `.agent-sessions-workbench`,
so the sessions stylesheet painted the input editor with
`agentsChatInput.background` while the window surface used
`input.background`, producing a visible color mismatch. Drive the editor
background from the same live theme color as the surface via a new
`--omni-input-editor-background` variable, and raise the rule so it wins
over the sessions rule.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>
Co-authored-by: meganrogge <merogge@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Allow sandboxed access to terminal output files

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix terminal sandbox service test environment

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Dmitriy Vasyura <dmitriv@microsoft.com>
Restore the existing Claude and Codex enablement paths and keep the experiment-aware root configuration change scoped to BYOK models.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
agentHost: address discovery review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: vritant24 <13074644+vritant24@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fix aquarium no showing
* Improve dictation cleanup reliability

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d367acf7-ee13-488e-bf9f-4aef2d274190

* Preserve dictation cleanup timeout diagnosis

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d367acf7-ee13-488e-bf9f-4aef2d274190

---------

Copilot-Session: d367acf7-ee13-488e-bf9f-4aef2d274190
* update footer details

* address comments
* mcp: preserve launch working directories across hosts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* mcp: address review and electron test failures

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* mcp: simplify provider working directory handling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Clear retained Codex runtime state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: address MCP discovery review feedback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: adapt Codex routing test to thread inventory

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: serialize Copilot chat discovery

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: deny disabled Claude MCP servers at startup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: preserve Copilot MCP cwd after re-enable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* MCP: address final cross-host review findings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Refactor browser sharing status into main process

* Fix test
* agentHost: Report billed AI credits per turn

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Separate turn usage tracking

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Agent Host changes for osortega/agents/chat-sticky-scroll-ux-review
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…330723)

* Add multi-root session folder picker for agent-host sessions

In a multi-root workspace, a new agent-host session must decide which
workspace folder leads. This adds a harness-owned decision for the
new-session Folder picker, seeded into the session `_meta` at creation
and frozen as a creation-time fact across reload.

The shared rule (folderPickerDecision.ts) counts folders that "qualify"
under a provider-specific predicate:
- 0 qualifying   -> hide the picker, keep the current selection
- exactly 1      -> hide the picker, pin that folder as primary
- 2 or more      -> show the picker so the user resolves the ambiguity

Per-provider criteria (folder pins itself as primary when it carries
config the provider only honors from the primary directory):
- Copilot: recursive `.github/hooks/**/*.json` scan
- Claude:  `.mcp.json`, or a non-empty `hooks` block in
           `.claude/settings.json` / `settings.local.json`
- Codex:   `.codex/hooks.json`

Client wiring resolves the decision into a picker-visibility update via a
pure, unit-tested function (resolveFolderPickerDecisionUpdate) and gates
the picker chip on a widget-scoped context key that defaults hidden, so
it never flashes visible-then-hidden while the decision resolves.

Adds unit tests for the shared decision, each provider's criteria, the
meta round-trip, and the client-side resolver.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refactor folder-picker decision logic and enhance tests for session metadata handling

* Enhance folder-picker agent tests with descriptor overrides for multiple working directories

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve explicit environment override precedence, gate BYOK session configuration as well as model publication, and register a null renderer channel when BYOK is unavailable. Add focused coverage without mutating the global configuration registry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* changes for sandbox toggle in copilot harness

* fixing tests
* agenthost: speed up rename chat tool

Avoid enumerating every session when resolving a rename target. Add a targeted registry lookup so rename validation and persistence remain awaited while lookup cost stays constant.

Fixes #331110

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agenthost: share live session metadata overlay

Reuse one helper for targeted session lookup and listSessions so both paths apply live state consistently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agenthost: make rename chat non-blocking

Return to the agent before session lookup and persistence complete. Serialize queued renames and log deferred failures while retaining targeted session resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agenthost: update merged rename test

Wait for the background rename independently while expecting the immediate tool result.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agenthost: simplify background rename

Start each rename independently without a sequencer and keep the existing tool description unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Reduce automatic rename noise

Mark reminder-driven rename calls so clients can hide successful administrative renames while keeping explicit renames and failures visible. Buffer leading model narration until the rename decision is known, discarding it on automatic rename and restoring it otherwise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Silence automatic rename output

Return no model-facing text from the background rename request and hide marked automatic renames from their initial streaming state so they cannot contribute a transient activity title.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Distinguish automatic chat renames

Add an automatic rename_chat argument so reminder-driven renames remain non-blocking while explicit renames await completion and surface failures. Keep the reminder and tests aligned with the execution contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Simplify automatic rename hiding

Derive automatic rename presentation directly from the resolved tool input instead of tracking rename lifecycle state and buffering model actions in AgentSideEffects. Automatic renames may remain visible while their arguments stream, then hide once automatic:true is available.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Silence automatic rename narration

Ask the model not to mention reminder-driven automatic chat renames while leaving explicit rename behavior unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Hide rename chat tool calls

Follow the GitHub app's pragmatic presentation policy by hiding rename_chat invocations throughout their lifecycle. The automatic argument remains responsible only for non-blocking execution semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Forbid automatic rename narration

Explicitly instruct the model not to say or mention the automatic rename before or after the tool call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Scope rename silence to automatic calls

Clarify that rename narration should be suppressed only when the model invokes rename_chat with automatic: true.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Limit rename PR to execution semantics

Restore the existing rename presentation and reminder behavior. Keep this PR focused on passing automatic:true for reminder-driven calls and using it to choose non-blocking versus awaited execution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Align automatic rename reminder tests

Update title-controller expectations for the concise automatic rename reminder.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Hide resolved automatic renames

Use rename_chat tool input to hide successful automatic calls after arguments resolve while keeping streaming, explicit, failed, and cancelled calls visible. Align tests with the updated reminder wording.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Hide rename while input streams

Hide rename_chat invocations by tool name during argument streaming, then reveal explicit calls or keep automatic calls hidden once input resolves.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Reuse rename tool name constant

Use SessionServerToolName.RenameChat when matching bare and transport-prefixed rename tool calls.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Align rename reminder wording

Update title-controller expectations for the stronger automatic rename narration instruction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Clarify automatic rename silence

Tell the model to emit no text before an automatic rename and not mention it afterward.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Target automatic rename preambles

Tell the model to invoke automatic chat renames silently without announcing the rename to the user.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Tighten automatic rename reminder

Combine the title and silent-tool guidance into one concise automatic rename instruction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Forbid automatic rename preambles

Make the automatic rename reminder explicitly require a silent tool call without a preamble.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Strengthen automatic rename reminder

Update the automatic rename instruction and align its title-controller expectations.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* agentHost: Polish automatic rename reminder

Use grammatical user-facing wording in the automatic rename host instruction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-enablement

Add BYOK enablement trace logs
Copilot AI and others added 12 commits August 18, 2026 00:50
* Initial plan

* Confirm before discarding chat request edits

Co-authored-by: justschen <54879025+justschen@users.noreply.github.com>

* fix tets

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: justschen <54879025+justschen@users.noreply.github.com>
Co-authored-by: justschen <justchen@microsoft.com>
* Initial plan

* Fix markdown header rendering in omni bar routing badge preview

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

* Add test for flattened heading preview with more-content indicator

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

* Append preview ellipsis as text node to preserve bare-URL autolinks

Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: meganrogge <29464607+meganrogge@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s Window (#331282)

* Share one model-selection policy between Workbench chat and the Agents Window

The Agents Window had its own implementation of "pick and remember the chat
model". Its precedence engine, `transitionModelSelection`, lived in
`workbench/contrib/chat/common/modelSelection.ts` but had exactly one production
caller — `vs/sessions` — so the shared location bought nothing while the two
surfaces were free to answer the same question differently. They did: a model
restored onto an empty conversation was a user's choice to Sessions and mere
spillover to Workbench, so `chat.defaultModel` overwrote it on one surface only.

`SessionModelSelection` now expresses the Agents Window on top of
`ChatInputModelSelectionController` through the `IChatInputModelSelectionRuntime`
seam, and `transitionModelSelection` is deleted rather than relocated.

Provenance becomes data instead of inference. `IChat.modelSource` records where a
chat's model came from and `ISessionsProvider.setModel` requires the caller to
state why it is setting one, so an automatic pick or a model a peer chat merely
inherited can be told from a model the conversation is meant to run on. Only the
latter outranks `chat.defaultModel`. `modelSource` is required rather than
optional: an absent value reads as "the conversation's own", which is the answer
that blocks the configured default, and a provider must not be able to claim it
by saying nothing.

A conversation's intended model is now held per conversation, keyed by the chat
resource, so one chat's choice is unreachable from another by construction rather
than by a scoping check.

Both surfaces run a shared conformance matrix. Every scenario field is consumed
through `conformanceInputs`, whose fields are all required, so an arm that stops
reading one fails to compile instead of quietly asserting a different question.
The matrix fences settled-catalog precedence and deliberately excludes
publication lifecycle, where the two surfaces still differ on purpose: Workbench
may display a stand-in while a model is pending, whereas Sessions waits rather
than writing that stand-in through to a provider.

Behaviour changes:
- A model a new peer chat only inherited no longer blocks `chat.defaultModel`
  from seeding that chat.
- Whether a conversation counts as empty is read from the chat rather than the
  session, so a brand-new peer chat in a finished session can still be seeded.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Shrink the Sessions model-selection adapter to what only it can answer

Three changes, none of which alter behaviour.

The adapter restated the controller's own precedence. `_canProceedWhilePending`
asked "may `chat.defaultModel` seed this conversation?" in terms of
`ChatModelSource` and chat emptiness, while `applyConfiguredDefault` asked the
same question in terms of selection reasons and pending intent. That is the drift
this series exists to prevent, still present in the one place both surfaces have
to agree. The controller now exposes `configuredDefaultToSeed`, which answers it
once; the adapter supplies only the conversation's authority, because the model
it would have to adopt to establish that authority is precisely the one still
unpublished.

Presentation moves to `sessionModelPickerState.ts`. What the picker shows is a
different question from which model the conversation runs on, and two other
modules already imported the option helpers from the selection file.

The provider-to-controller vocabulary moves to `sessionModelProvenance.ts`, where
the collapse from four `ChatModelSource` answers to two `ModelSelectionAuthority`
ones can be read on its own.

The adapter drops from 628 to 523 lines and is now about selection alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Hold the Agents Window's model selection per conversation, not per input

The intended model was already per conversation, but the two facts that describe
it — whether the conversation has been seeded, and the authority behind its model
— were still single fields on the input, kept correct by re-assigning them on
every rebind. That is the arrangement the per-conversation intent was chosen to
avoid: correct only for as long as each rebind path remembers to clear it, and
silently wrong for the incoming chat the first time one does not.

All three now live together in a `ConversationModelSelection` record held per
chat resource, so one conversation's answer is unreachable while another is
bound, by construction rather than by a reset. Clearing on rebind goes away with
it: when no session is bound there is nothing to clear, because nothing that
describes a conversation lives outside its own record.

What remains on the input is a snapshot of the provider and the bound chat —
models, model target, emptiness, binding identity — reassigned on every refresh
rather than carried across passes.

No behaviour change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Address review: key setModel by chat, and prune peer selections on the way out

`ISessionsProvider.setModel` took only a session id, but the model belongs to a
chat. Both providers had to guess which one: the Copilot provider resolved a
grouped session id to the group's first chat, and the Agent Host consulted
whichever session was globally active, falling back to the main chat. A picker
shown in a visible peer chat could therefore write to a different conversation —
the same model selection this series otherwise keeps strictly per chat, lost at
the last step. It now takes the chat resource, matching `sendRequest`, and both
providers resolve the chat instead of inferring it.

Two agent-host tests had been setting the globally active session purely so the
inference would pick their peer chat. They now name the chat, which is what they
were testing all along.

`_activeChatResource` remains for `setAgent`, which is keyed the same way and has
the same weakness; it is documented as a guess so the next caller does not take
it for an answer.

Separately, peer chats' remembered model selections were pruned only on the
multi-chat path. Removing the last peer takes a session down the single-chat
path, which returned before reaching the prune, so those selections were never
released. Pruning now happens before either branch returns, and only for peers
this session had already materialized — a selection recorded for a chat whose
state has not arrived yet is waiting, not stale.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Collapse the third model-provenance vocabulary into the reason it became

Model selection carried three vocabularies for one question. A provider said
where a chat's model came from (`ChatModelSource`: user, restored, inherited,
automatic), the controller recorded how it applied one (`ModelSelectionReason`,
whose `RestoredChoice` and `SessionRestore` are exactly "the conversation's own"
versus "standing on it"), and in between sat `ModelSelectionAuthority`, a
two-value type saying the same thing a third time.

It was never anything else. `restoreReasonFor` existed only to turn an authority
back into one of those two reasons, and all three of its callers assigned the
result straight to the reason the controller actually keeps. Reading the flow
meant translating between three type systems to follow one bit.

`ModelSelectionAuthority` and `restoreReasonFor` are gone. The two entry points
that took an authority now take a `RestoredModelReason` — the same two values,
named in the vocabulary the controller already uses — so a caller states the
reason it wants recorded and that is what gets recorded.

Two vocabularies remain, which is the number the boundary needs: what a provider
reports, and what the controller records. `sessionModelProvenance` translates
between them in one place.

No behaviour change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Apply a restored model under its own reason, and stop overloading "intent"

A model the conversation chose but whose pool publishes it under another
identifier was applied before the reason for applying it was recorded. Applying
writes the model out through the runtime, and a surface that persists it reads
the reason while that call runs — so the Agents Window wrote the conversation's
own model under whatever reason the previous conversation left behind, which on
a rebind is nothing at all. That lands in the provider as `Automatic`, reads
back as spillover, and lets `chat.defaultModel` overwrite it: the exact failure
this series exists to prevent. Every other apply site already set the reason
first; this one is now consistent with them, with a test that asserts the reason
in force at the moment the model is written.

Three different things were called "intent": the conversation's intended model,
the holder it lives in, and a programmatic selection waiting for its model to be
published. Only the first two are the same idea. The third is now
`_pendingProgrammaticSelection`, and its two byte-identical accessors —
`hasPendingIntent` and `hasPendingProgrammaticSelection` — are one.

`ModelSelectionReason.NoModels` was never assigned or compared; its only role was
being excluded by `ModelSelectionApplyReason`. Both are gone, so the reason a
model was applied has one type rather than two.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Ask the model-selection seam questions instead of Workbench's inputs

`IChatInputModelSelectionRuntime` was shaped around Workbench's widget rather
than around the question both surfaces ask. It took `location` and
`getCurrentModeKind` so the controller could work out for itself whether a model
was usable, which only Workbench has real answers for. The Agents Window filled
the gap with four constants — `Ask`, `false`, `Disposable.None`, and an empty
function — and a reader had to trace those through generic helpers to discover
they meant "no restriction".

The seam now asks directly. `isModelSupportedHere` and `getDeclaredDefaultModel`
replace `location` and `getCurrentModeKind`, and each surface answers in its own
terms: Workbench by its mode and where it is shown, the Agents Window by saying
a session runs whatever its provider published. `subscribeToModelChanges` and
`restoreModelConfiguration` are optional, because a surface that drives its own
reconciliation and has no per-model configuration should omit them rather than
stub them. Sessions' runtime now contains no stubs.

`shouldResetModelToDefault` and `resolveModelFromSyncState` take that predicate
instead of a context object, in the same order they checked before, so the
surface-specific part is stated by the surface and the pool and session checks
stay where they were.

No behaviour change: `Ask` mode and the `Chat` location both short-circuit to
true, so the constants the Agents Window used to pass already meant what it now
says outright.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Make resetting to the default forget the preference it overrides

`resetLanguageModelToDefault` cancelled any pending programmatic selection and
selected the default, but left the conversation's intended model in place. That
intended model is the remembered preference the reset exists to override, so the
next time the catalog published, reconciliation restored it and the reset was
silently undone. The `ConfiguredDefault` guard that would otherwise stop this
does not apply, because a reset with no `chat.defaultModel` configured leaves the
reason as `FirstAvailable`.

Reachable from the automation dialog, which builds a fresh input whose intent is
already seeded from the stored preference, then resets before applying the
automation's own model. With no saved model to apply afterwards, the preference
came back on its own.

The controller now owns the whole operation as `resetToDefault`: abandon the
pending selection, forget the intended model, take the default. A test asserts
the default survives a subsequent catalog change; it fails without the forget.

`clearPendingProgrammaticSelection` goes with it, having had no caller left.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* State the model-selection rule where the code implementing it lives

The precedence this file owns was documented only in fragments, spread across the
comments of the methods that enforce it. A reader could learn what each branch
did without ever meeting the rule the branches exist to serve.

The header now states it once: a model on a conversation is either that
conversation's choice or spillover; `chat.defaultModel` seeds the second and
yields to the first; `isInConversationModelChoice` is the line between them, and
every "may the default win here?" question routes through it. It also names the
three phases each public operation belongs to — initialize, reconcile, sync —
and says which two operations deliberately sit outside them.

Finally it records why the two surfaces are allowed to differ, and only here:
Workbench chat may show a stand-in while a model is unpublished because the cost
of being wrong is a repaint, while the Agents Window writes through to a backend
and so must wait.

Documentation only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Keep one record of how a chat came by its model

Selection kept two. The controller recorded a reason on the conversation's
intended model; the Agents Window kept a parallel `ChatModelSource` per
conversation, updated on the same events, with its own change detection beside
the controller's echo suppression. Two records of one fact, held in step by
hand.

The provider source is now derived from the reason. What has to survive that
round trip is whether the model speaks for the conversation, and it does:
every reason maps to a source that maps back to a reason on the same side of the
choice/spillover line, so echo suppression and the `chat.defaultModel` rule are
unchanged. A user's own pick returns as `Restored` rather than `User` once
written and read back — both are the conversation's own, so no outcome differs;
only the label is coarser. The test that pinned the old label now asserts the
property the rule actually turns on.

Deleting the field also removes the record-before-write and roll-back-on-throw
dance at both provider writes, which existed only to keep the copy in step.

Two further changes in the same spirit:

`_applyModel` now takes the reason it is applying under. It records it before
handing the model to the surface, because a surface that persists reads the
reason during that call — the shape of a bug already fixed once on this branch.
Sites that deliberately carry the current reason over, such as canonicalizing an
identifier, now say so instead of relying on the absence of an assignment.

`requiresCustomModels` leaves the seam. It was the last member phrased as one
surface's inputs rather than as the question being asked; it is now an optional
`isAwaitingSessionModels`, which the Agents Window omits because a provider
snapshot is already the session's own pool.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Leave only shared work on the shared selection controller

The controller had grown entry points that only Workbench chat ever called, so
reading it meant deciding for each one whether the Agents Window relied on it.
Five did not, and did not need to be there:

- `beginSessionSwitch`, `endSessionSwitch` and `restorePerTypeModel` existed to
  latch a single boolean across a widget handshake — a decision made before the
  view model arrives and acted on after. That is the widget's business, and the
  latch now lives beside the handshake it belongs to. What the controller keeps
  is `beginConversationSwitch`, which both surfaces call for the part that is
  shared: dropping what spoke for the outgoing conversation.
- `resolveDraftModel` and `reinitializeIfOutsidePool` were compositions of things
  the widget already knows — its own catalog, its configured default, its current
  model — expressed through the controller rather than directly.

`revalidateForSessionType` stays despite also being Workbench-only: it reaches
the selection reason and applies models, so moving it would mean widening the
controller to let it back in, which is the opposite of the point.

The public surface goes from 22 members to 17, and the per-type restore latch
leaves the shared class entirely rather than being re-housed behind an accessor.
The test that covered that latch goes with it; what it asserted is now the
one-line expression that sets it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Reuse the per-type restore rule instead of restating it

Moving the restore latch into the widget inlined its expression, leaving
`shouldRestorePerTypeModelOnSessionSwitch` — which already stated the same rule,
with the reasoning for it and its own tests — unused. The widget now calls it, so
the rule has one statement and its tests cover live code again.

Also drops two references to `beginSessionSwitch` left behind in the policy
header and in `beginConversationSwitch`, which no longer resolve.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Stop a reopened session being switched to the last model used elsewhere

Reopening a finished session could silently move it to whatever model was picked
most recently anywhere in the profile. A GPT session reopened after visiting an
Opus one came back on Opus, and the write went through to the backend, so it
stayed there.

Two things combined to cause it. An agent-host session reports `modelId` as
undefined until something says otherwise, and nothing did: the provider hydrates
the selected agent from the default chat's persisted draft but never the model.
Model selection then read "no model" as "this conversation has never chosen one",
which is the state a remembered preference exists to seed.

Both halves are fixed.

`_hydrateModelFromDraft` mirrors the agent hydration already beside it, reading
the model back from `ChatState.draft.model` and recording it as `Restored` —
what it is, the conversation's own model read back from where the host kept it.
Like its counterpart it is one-shot and guarded, so it cannot override a
selection made in the meantime.

Independently, a conversation that has already run is no longer *given* a model.
Its own model may simply not have arrived yet, and a profile-wide preference is
not an answer for it: showing one keeps the picker from being blank, but writing
one changes what the conversation runs on rather than describing it. Only a
conversation that has yet to run can be seeded. A pick the user makes is
unaffected — that is an answer for this conversation, and still writes.

The second half stands on its own: it closes the window before hydration
completes, and covers providers that report no model at all.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Do not let the configured default take a model the user just picked

Selection asked the controller whether `chat.defaultModel` could seed while a
wanted model was still unpublished, but answered its own question first: with no
model yet on the chat it passed `SessionRestore`, which reads as "nothing has
claimed this conversation". That overrode what the controller already knew.

So between a user picking a model and the provider echoing it back, a catalog
refresh that dropped the pick let the configured default win — the case this
series exists to prevent, reached through the one call that talked over the
controller instead of asking it.

It now passes what it actually knows, and `undefined` means "you decide", which
is what the parameter was documented to mean. The controller then sees its own
pending user choice and holds.

The wrapper this lived in was a single line with one caller and is gone with it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Say it once, in plain English

The comments had grown into essays. The controller was 31% comment with ten
blocks of six lines or more, the adapter 28%, and `sessionModelProvenance.ts` was
56% — more comment than code. Much of it restated the line below, or explained
the same rule two or three times in different words.

Cut to one-liners wherever the code already says it, keeping length only where a
comment records something the code cannot: why a reason is set before a value is
handed over, why an echo has to be ignored, why one surface waits where the other
shows a stand-in. The file header keeps the rule the file exists to enforce and
drops the tour of the API around it.

Also removed, all unused: the `source` discriminant on the remembered selection,
which nothing read, and the `conversationKey` parameter to `_applySessionRestore`.
Parenthesised a ternary whose `||` chain read as though it bound the whole
expression, and corrected the `setModel` signature in the Copilot provider's
README.

Two tests asserted the wording of diagnostic log lines, which no user can observe
and any rewording breaks. They now assert the behaviour they were reaching for.
Two others were renamed to what they actually check: one claimed to be about
enabling send while asserting empty picker state.

No behaviour change. 254 lines shorter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65

* Show a stand-in rather than nothing while a reopened chat's pool publishes

Reopening a chat that had already run, while its agent host was still
connecting, left the model picker blank and the composer refusing to send until
the catalog settled.

The desired-model probe falls back to the profile-wide preference when the chat
has no model of its own yet, which is exactly the window before the provider
hydrates one. An agent-host vendor deliberately reports an absent model as
`pending` rather than `unavailable` while its catalog is in flight, so the probe
came back pending, and the wait blanked the picker: nothing is shown while a
selection is pending, and a pending selection also blocks send.

The wait exists to keep a transient stand-in from being written through to the
backend and changing what the conversation runs on. But a conversation that has
already run is display-only, and `_pushModelToProvider` already withholds every
write for those. So the wait was guarding a write that was never going to
happen, and charging the picker for it.

It now applies only where there is something to guard. A conversation that would
be written to still waits; a display-only one falls through and shows the
nearest thing its pool offers, which is what the sibling case immediately above
it in the tests already expected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadcb0d5-d0b7-4bd2-a092-75982f51c63a

* Say where a model came from without saying "provenance"

"Provenance" is a word most readers have to stop and translate, and it earned
its place here only by being short. The thing it names already has a plainer
name in the code it describes: `ChatModelSource`, and `IChat.modelSource`.

So the file that translates between a provider's account of a model and the
controller's is now `sessionModelSource.ts`, matching the type it converts, and
the comments say "where the model came from" or "credited to the wrong source".
Nothing else changes: same functions, same call sites, same behaviour.

Left alone is `derives automation provenance from the provider run ledger`,
which predates this work and is about something else.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadcb0d5-d0b7-4bd2-a092-75982f51c63a

* Cover the two behaviour changes that had no tests, and drop a dead helper

Three loose ends from review.

`findDefaultModel` no longer has a production caller. The controller asks the
surface for its declared default and falls back to the first model itself, which
is the same rule spelled out at the seam, so the helper is gone. Its behavioural
callers in the tests were computing "what would a reset land on", so that
composition now lives in the test file beside `computeAvailableModels`, which
already does the same for the model pool. The two suites that only exercised the
helper itself went with it: the rule they covered is asserted against the real
controller, which is where it now lives.

Two behaviour changes shipped in this series without tests, both of them forced
by the rule that a conversation which has already run is never given a model —
without them such a chat would have no model at all rather than the wrong one.

`_hydrateModelFromDraft` now has the pair its counterpart already had: a resumed
session picks its model back up from the persisted draft as `Restored`, and a
live pick still wins over a later draft snapshot.

`forkChat` is covered for starting the new chat on the source chat's own model
rather than the session-level default it used to take, asserted through the host
call and the new chat's input state.

That last test does not assert the forked chat's `modelSource`, which does not
come back as `Inherited`: the write reports no entry for the chat the catalog
just created, and re-emitting the catalog does not seed it either. Left as-is
rather than guessed at, since it is the same path `createSideChat` and
`createNewChat` take and none of them assert it.

Also corrects SESSIONS.md, which announced two invariants and listed three.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadcb0d5-d0b7-4bd2-a092-75982f51c63a

* Assert the inherited source a fork records, and stop the harness evicting it

The fork test stopped short of asserting the thing the change is for: that the
new chat records its model as `Inherited` rather than as its own. It was left
out because the assertion failed, and the reason was not understood.

The reason was the harness. `setupMultiChatSession` announced a session with a
`SessionAdded` notification but never registered it with the mock host, so the
`getSessions()` that follows started a refresh whose authoritative session list
came back empty — and an empty authoritative list evicts the adapter the
notification had just created. The catalog was materialised on that adapter; the
inherited-model write then landed on it after eviction, on an instance nothing
reads, which is why the chat surfaced with no source and why re-emitting the
state did not seed it either.

Registering the session before announcing it makes the two consistent, which is
what a real host reports, and the assertion then holds: the forked chat carries
the source chat's model with `ChatModelSource.Inherited`, so `chat.defaultModel`
may still seed it. No other multi-chat test changes behaviour.

Worth noting what this does not fix. `createNewChat`, `forkChat` and
`createSideChat` each capture the adapter before awaiting the host and use it
again afterwards, without rechecking that it is still the cached one. The
harness reached that window deterministically; production would need an eviction
mid-creation to do the same, but nothing prevents it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dadcb0d5-d0b7-4bd2-a092-75982f51c63a

* Say "the chat's own" or "carried over", and say it once

Model selection acted on a two-state question — is this model the chat's own? —
through four `ChatModelSource` values, a lossy mapping in each direction, and a
third spelling in the conformance harness.

Collapse the enum to `Chosen` / `CarriedOver`. `User` and `Restored` always
answered alike, as did `Inherited` and `Automatic`, and `sourceForReason` proved
it: expanding a reason back into four values was only ever
`isInConversationModelChoice(reason)` wearing a switch statement. It is now
exactly that, so `sessionModelSource.ts` has nothing left to translate and goes
away.

Give the rule one home: `isChatOwnModel` says that an absent source counts as
owned, and both directions read it rather than restating it.

Shorten the SESSIONS.md section to what a reader needs, and drop "spillover"
for plain English throughout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bcbe1455-6e2d-4d70-a125-dc342b293b5b

* Do not let a stand-in take the model a chat is waiting for

Three review findings.

A chat that falls back to a stand-in writes it back as carried over. Rebinding
the chat then read that stand-in as its answer and dropped the model it was
waiting for, so the model was never reclaimed and `chat.defaultModel` was free
to seed over it. The old guard only caught this when nothing had been bound in
between: it asked whether the arriving model was the one on screen, and after a
peer visit the screen belongs to the peer. Ask the bound conversation instead —
it is still waiting, and a carried-over model is not an answer for it. Workbench
cannot say where a draft model came from, so it keeps the on-screen test as a
second way in.

`createFixtureActiveSession` supplied a chat with only `resource`, which threw
once selection started reading `status` and `modelSource` — all twelve
prompt-options fixtures errored, failing the fixture job.

`forkChat` fell back to the session's model for a peer whose own model this
client does not know, which after a reload forks it onto a model it was never
running. State none and let the host answer, as `createSideChat` does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bcbe1455-6e2d-4d70-a125-dc342b293b5b

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 703445d9-b313-4faa-abf9-1fc939a01c65
Copilot-Session: dadcb0d5-d0b7-4bd2-a092-75982f51c63a
Copilot-Session: bcbe1455-6e2d-4d70-a125-dc342b293b5b
Log the effective BYOK state and its environment and synchronized root-config inputs when publishing models and building session configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep BYOK enablement diagnostics available at trace level without adding routine info-level noise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing (#331402)

* Refactor hook scanning logic and improve folder picker decision handling

* Enhance folder picker decision logic to honor filesystem casing and improve hook detection documentation
Document that the BYOK environment variable is an explicit override and that synchronized root configuration supplies the normal enablement value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Chat: navigate back to session list with mouse

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Chat: consume mouse back navigation event

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Chat: capture mouse back navigation globally

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…models-issue

agentHost: sync BYOK experiment enablement
@pull pull Bot locked and limited conversation to collaborators Aug 18, 2026
@pull pull Bot added the ⤵️ pull label Aug 18, 2026
@pull
pull Bot merged commit 7fee815 into code:main Aug 18, 2026
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.