Logan Ramos
90d · built 2026-09-08
Performance
What Logan Ramos shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+39.0engineers
delivers like 40.0 (40.0x pre-AI)
Output (ETV)
83.1ETV
+992.4% vs 7.6 prior
Features share
27.5%
−7.7 pp vs prior window
Fixes share
21.3%
−16.7 pp vs prior window
Work mix
27.5% Features17.6% Maintenance32.6% Tests1% Docs21.3% Fixes
120 commits over 90 days, ending 2026-09-08.
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 80 %
- By Features share
- Top 66 %
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 10 by ETV in the last 90 days.
- 5.4ETVExperimental model picker redesign (#334332) * Add experimental tabbed model picker Redesigns the chat model picker behind `chat.experimentalModelPicker` (off by default) so the new and old designs can be compared side by side. The separate model, context, and thinking effort pickers become one surface: a tabbed list with a detail card that shows thinking effort, context, and pricing that updates as those change. Destinations are capped at two, built-in and user-provided, since almost no one has more than one BYOK provider. Sections are Pinned, a short suggested list, and the rest folded away. Also adds a shared Switch widget and a segmented control, both reused by the picker and the customizations UI, and support in the action list for tab bar actions, icon-only tabs, footers, welcome bodies, and in-place item updates. Includes an unrelated fix for the DOM sanitizer, which now replaces its Trusted Types policy when the realm that created it goes away. * Fix some small issues with the new model picker * Model picker * Review feedback * Fix Monaco editor build for the sanitizer policy replacement The stale-policy replacement was cast to the global `TrustedTypePolicy`, but dompurify's config types that property with the `TrustedTypePolicy` imported from `trusted-types/lib/index.js`. The two normally unify, so the regular build accepts it. The editor tree shaker compiles against a synthetic root where `trusted-types` resolves through both a relative and an absolute path, leaving two declarations whose private `brand` makes them nominally incompatible, and `editor-distro` failed to compile. Name the property's own type instead, so only one declaration is involved however `trusted-types` resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address feedback and fix tests * Fix provider icons * Fix ResizeObserver loop in the model picker chip `_renderLabel` runs from a resize-driven autorun. Measuring the chip there cleared `minWidth`, read `scrollWidth`, then wrote `minWidth` again, so every ResizeObserver pass dirtied layout twice and never settled. The NewSessionCompactAutoModel fixture failed to render with "ResizeObserver loop completed with undelivered notifications". Take the fixed widths from main instead, including the narrower floor for the Auto label that #334128 added alongside that fixture. This resolves the merge conflict in this hunk the other way; the measured floor read better in the abstract, but it cannot be computed from inside the resize callback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Restore MCP server switch styling The plugin list moved to the shared `Switch` widget and the hand-rolled `.plugin-enable-switch` CSS was deleted with it, but the MCP server list still built that markup by hand. With no rules left to match, its switches rendered as bare unstyled buttons: the McpServersTab fixture lost the filled pill and its thumb entirely. Move the MCP list onto `Switch` too, so both lists share one control. This is what the screenshot diff on this PR was reporting; accepting those hashes would have pinned the unstyled rendering as the baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>github.com-microsoft-vscode · 74a1af8a · 2026-09-03
- 4.1ETVRefactor model picker architecture (#326052) * Refactor model picker * Some linters and formatters * Cleanup some old tests * Simplify model picker state and configuration * format stuff * Address model picker review feedback * Update blocks-ci screenshot hashes * Simplify model picker architecture termsgithub.com-microsoft-vscode · f38816eb · 2026-07-16
- 3.5ETVChat usage tracking improvements (#328012) * Agent host usage tracking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e3463606-86bf-4c41-ab25-936dc1c9d952 * Cleanup some model picker stuff * Address comments --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e3463606-86bf-4c41-ab25-936dc1c9d952github.com-microsoft-vscode · 5c3112aa · 2026-07-29
- 3.3ETVShare one model-selection policy between Workbench chat and the Agents 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-dc342b293b5bgithub.com-microsoft-vscode · d9a8a27a · 2026-08-18
- 3.0ETVCoalesce content exclusion fetches to stop exhausting the GitHub API rate limit (#328268) * Coalesce content exclusion fetches to stop exhausting the GitHub API rate limit Every caller that discovered a new repository triggered a refresh of the content exclusion rules for *every* known repository, so request volume grew quadratically with repository count. In a workspace with many git repos (an AOSP checkout, in the reported case) this produced ~16k requests to api.github.com in 30 minutes, exhausting the account's 5k/hour REST budget and starving everything sharing it, including Copilot token refresh. The endpoint is https://api.github.com/copilot_internal/content_exclusion, so it draws on the user's ordinary REST quota rather than a CAPI budget. - Coalesce per repository using shared DeferredPromises, a short batching window and a bounded-concurrency Limiter, so each repo is fetched at most once per TTL and each caller only waits on the repos it asked for. A regression test measures 75 requests -> 3 for 26 repositories. - Only cache rules on a successful response. Empty placeholder rules were written on discovery and left in place on failure, making a failed fetch indistinguishable from "this repo has no exclusions" and preventing a retry for 30 minutes, so exclusions silently stopped applying while rate limited. - Only memoise a negative verdict once the relevant rules actually loaded, which otherwise left files checked during an outage permanently allowed. - Add a shared rateLimitBackoffMiddleware covering 429 and quota-exhausted 403 responses, honouring Retry-After and x-ratelimit-reset, and move both RemoteContentExclusion and CloudSessionApiClient onto it. This replaces a third hand-rolled copy of the same backoff logic. - Precompile glob patterns, track the regex rule count directly, and only invalidate memoised results when rules that could change an outcome arrive. Fixes #322275 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043f * Address review feedback on cache invalidation and request lifecycle Five correctness issues raised in review, each with a test that fails against the previous implementation. - rateLimitBackoffMiddleware: a response that was already in flight could clear a block established by a concurrent rate-limited request, letting later calls reach the server during the window the server asked us to wait out. The backoff is now only reset once the active block has elapsed. - isIgnored returned a memoised verdict before reaching the staleness check, so a URI that had been evaluated once never triggered a refresh and could miss newly added exclusions indefinitely. Verdicts are now tagged with a rule generation and are only trusted while the rules behind them are unchanged and unexpired. - applyRules only invalidated verdicts when the incoming rules were non-empty, so a refresh that removed the last rule left files excluded permanently. Incoming rules are now compared against the previous set, which also avoids invalidating on an unchanged refresh. - drainPendingRepos cleared the pending map before its batches completed, so a lookup arriving while a request was slow queued a duplicate fetch every batching window. Entries now stay registered until their request settles, and are removed only if still owned by that attempt. - dispose only settled repos still queued. Limiter.dispose drops queued factories without running them, so with more than five batches the callers awaiting them never resolved. Pending entries are now settled before the limiter is disposed, and enqueues after disposal resolve immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043f --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32f94728-158b-4639-b789-a5dd483f043fgithub.com-microsoft-vscode · 9bbf5e43 · 2026-07-31
- 2.7ETVConsolidate chat input notices behind one arbitration host (#329284) * Consolidate chat input notices behind one arbitration host Five surfaces render above the chat input (notifications, voice and dictation introductions, getting-started tips, and the sub-session tip), and each new one had bolted its own suppression hack onto the previous: a CSS `!important` sibling selector, boolean flags plumbed from the input up to two parent widgets, and onboarding reaching across to mutate the tip container's inline style. Replace all three with a single per-input `ChatInputNoticeHost` that arbitrates precedence (notification > onboarding > tip) via counted claims, and move the tip rendering duplicated across `ChatWidget` and `NewChatWidget` into one shared `ChatInputTipPresenter`. Two bug fixes fall out of this: - `getWelcomeTip` recorded permanent mode-exclusion state before its location gate, so inline chat and dialog inputs could silently exclude a tip from the real chat panel forever. - "Toggle Focus Between Notice and Input" never worked in the Agents window, whose composer is not an `IChatWidget`. A small window-level hub resolves the most recently focused input instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fef4412-a23d-4321-82fd-ce1bf349cb90 * Address review feedback on chat input notice arbitration - Track onboarding visibility per host registration instead of aggregating service-wide state, so a card docked in one input no longer keeps another input's onboarding lane claimed. - Make onboarding react to notice-host occupancy: a card stands down when higher-precedence content takes the space and comes back when it is given up, so precedence holds for as long as the card is up rather than only at show time. - Scope notice focus targets per lane and wire them for notifications and onboarding cards, so "Toggle Focus Between Notice and Input" matches its label and cannot focus a displaced notice. - Replace the descendant-dependent :has() tip seam selector with an explicit state class on the tip container. - Drop stale positional JSDoc from both registerHost declarations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix tip presenter disposal leak and notice lane churn - ChatInputTipPresenter rebuilt its tip into already-disposed holders: the autorun is registered after the MutableDisposables, so releasing the leading tip's claim during disposal re-ran it and the replacement store, claim and DOM were silently dropped by the disposed MutableDisposable setter. Guard update() on disposal, and clear the container so the rendered tip node is not left orphaned (ChatTipContentPart does not remove its own DOM). - ChatInputNoticeHost.setOccupied released a lane before re-claiming it, and those observable writes are not batched, so swapping the notice in a lane let lower-precedence content flash into the gap. Claim before releasing. - newChatWidget: drop the redundant post-construction tip render and make the tip container a local. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Detach notice lane leases before disposing them Releasing a claim notifies observers synchronously. `DisposableMap.deleteAndDispose` disposes before deleting, so a reaction that re-claimed the lane during that disposal had its fresh lease deleted from the map undisposed, stranding the claim and holding the lane forever. Remove the lease from the map first, then dispose it. Regression test included; it fails against the previous ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Arbitrate introductions through the notice host instead of exclusion groups The lane host arbitrated between categories, but voice-vs-dictation exclusivity - arbitration within the onboarding lane - was a second mechanism: a static process-global exclusion-group map with its own preempt and first-run handback. Both implemented "put the other one away and give it its showing back", which is the coupling this PR set out to remove, on exactly the kind of global state that caused the per-host visibility bug. A claim now learns when it is the notice actually on screen: the newest claim in the lowest occupied lane leads, everything else stands down but keeps its claim. Recency within a category and precedence between categories become one thing. Introductions claim the space directly and build only once they lead, which removes the rest of the bookkeeping: - The first-run key is written when the card is really put in front of the user rather than when a card is constructed, so being deferred or put away no longer spends the one showing. `_consumedFirstRun`, `_restoredFirstRun` and the handback they guarded are gone, and with them the unbounded-restore hazard the bound existed to cap. - Standing down hides the card instead of tearing it down, so in-flight state survives and `shown` telemetry is reported once per showing rather than once per restore. - A card put away for a peer now returns when that peer goes away, matching the behaviour notifications already had. ChatInputOnboarding drops from seven interacting pieces of state plus a static registry to four. Card construction failures are reported rather than rethrown, since they now run from the host's leading notification and unwinding through it would strand the claim. Behaviour note: an explicitly requested introduction now waits for a higher-precedence notification to clear instead of rendering underneath it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Settle notice leadership against re-entrant claims Standing down is a real side effect - it moves focus and hides DOM - so a claim's callback can change who owns the space while the host is still announcing. The previous code captured the leader up front and announced it unconditionally after standing the old one down, so a claim replaced in the meantime was still told it leads, leaving two notices believing they were on screen. State is now re-read after every single notification, with a bounded loop that reports rather than spins if claims never settle. Also fixes a stranded claim on the failure path: `build()` runs from the host reporting that we lead, before the lease is in hand, so a card that threw during construction called `hide()` while `claim.clear()` had nothing to clear - and the lease was then stored anyway, holding the onboarding lane forever with no card, silently suppressing tips. The first leading answer is now held until the lease is stored. Both found by review; each has a regression test that fails against the previous code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Do not install an introduction that was taken down while building The factory can synchronously take the card down - dismissing straight away, or because the input it is docked to went away mid-construction. Nothing revalidated after it returned, so the card was installed into an unregistered host, its one first-run showing was spent and it was announced, all for a card that should have been gone. Revalidate that this request is still the one meant to be on screen before committing it. Found by review; regression test fails against the previous code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * More cleanup of the arbitration host * Cleanup tests * Stub the notice hub service in chat component fixtures ChatInputPart and NewChatInputWidget now take IChatInputNoticeHubService, which the fixtures' instantiation service did not provide. createInstance threw while building the input, so the chat widget never rendered and the resize-observer harness never appeared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48a08153-48f7-45d6-96e7-bf5bf918499b * Stub the notice hub service in the inline chat zone fixture Same gap as the shared chat fixture services: the inline chat zone fixture registers its onboarding stubs inline rather than through registerChatFixtureServices, so it also needs IChatInputNoticeHubService for ChatInputPart to instantiate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 48a08153-48f7-45d6-96e7-bf5bf918499b --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fef4412-a23d-4321-82fd-ce1bf349cb90 Copilot-Session: 48a08153-48f7-45d6-96e7-bf5bf918499bgithub.com-microsoft-vscode · 60033019 · 2026-08-11
- 2.7ETVAdd experiment driven inline model feedback survey (#331850) * Add experiment driven inline model feedback survey Adds a survey that can be attached to chat responses through an experiment treatment, so a survey can be authored, changed, or retired without shipping code. When one applies, a combined thumbs up and down control replaces the usual helpful and unhelpful actions in the response footer and opens a short multi step survey beneath it. The survey is fully described by a versioned JSON payload: which responses it applies to, when it may open on its own, and its steps. Answers are reported as each step is taken, so surveys the user abandons still produce data, and they land in GitHub restricted telemetry through a command the Copilot extension registers. Manual activation is never rate limited. The pacing rules in the payload govern only surfacing the user did not ask for: a weekly cooldown, a per session cap, a probability that ramps with use, and a trigger for switching off the surveyed model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aaf67de5-2dc8-437c-961b-257c488475a7 * Fix fixture services and address review feedback Registers the survey service with the shared chat fixture services, which the component fixture tests build a real chat list renderer from. Without it the resize observer harness fixtures failed to load. Also from review: - Re-check a cached survey against the current config and the feedback setting, so retiring a treatment or turning feedback off takes effect. A survey the user is part way through is left alone, since a treatment that briefly resolves to nothing must not take a form away mid answer. - Release per session state when a session is disposed, rather than holding it until the treatment changes or the window closes. - Move focus to the close button after submitting, so keyboard users are not left on the document body while the acknowledgement is showing. - Report the feedback control as expanded rather than pressed, since it discloses a panel rather than holding a state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aaf67de5-2dc8-437c-961b-257c488475a7 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aaf67de5-2dc8-437c-961b-257c488475a7github.com-microsoft-vscode · 02344021 · 2026-08-20
- 2.2ETVImprove copy and paste architecture across agent and editor window (#329455) * Improve copy and paste architecture across agent and editor window * Address comments * Fix paste as CSS * Share associated chat resource data between associations An associated URI with a stable id could be handed to several consumers, so disposing one of them released data another was still reading. Associations now share a reference-counted entry that is released by the last of them, and releasing an entry also drops the sessions' references to it so a session's set no longer grows for the lifetime of the window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>github.com-microsoft-vscode · 0d5a81a0 · 2026-08-11
- 2.1ETVMore model picker improvements (#334674) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>github.com-microsoft-vscode · 291338d4 · 2026-09-05
- 2.0ETVImprove copying chat output to external apps (#328466) * Improve copying chat output to external apps * address commentsgithub.com-microsoft-vscode · f303fd73 · 2026-08-05