# Desktop Extension And Model Capability Fixes

## Goal

Make the Desktop "Skills and Extensions" view reflect Pi's effective extension configuration, and make model capability guidance accurate without coupling model IDs to provider-specific request protocols.

## Scope

The change covers the seven findings from the 2026-07-14 review:

- Preserve model editor focus while entering a model ID.
- Separate GPT-5.6 capability metadata from provider transport compatibility.
- Parse npm, git, URL, and local package sources correctly.
- Apply capability presets explicitly and completely.
- Honor Pi package filters and convention-based extension discovery.
- Include trusted project-level extension packages.
- Show whether a recognized model preset is already applied and expose its thinking-level mapping.

Host UI bridge changes already present in the worktree are outside this scope.

## Extension Discovery

Desktop will not maintain a second implementation of Pi package resolution. Extension discovery is split into a synchronous snapshot read and an asynchronous refresh:

1. Capture immutable `normalizedCwd` and `projectTrusted` values from `ctx.cwd` and `ctx.isProjectTrusted()` and build the context key.
2. `getExtensionSnapshot()` synchronously returns the cached list, loading state, and sanitized error for that exact context key. Initial window construction uses this snapshot and never blocks on package resolution.
3. `refreshExtensions()` calls `SettingsManager.create(normalizedCwd, getAgentDir(), { projectTrusted })`, then `new DefaultPackageManager({ cwd: normalizedCwd, agentDir: getAgentDir(), settingsManager })`.
4. Immediately inspect `settingsManager.drainErrors()`. A global settings error, or a project settings error while `projectTrusted` is true, fails the refresh without replacing a successful cache entry.
5. Call `resolve()` with an `onMissing` callback that returns `"skip"`, so opening Desktop never installs packages or performs a package update. Inspect `drainErrors()` again after resolution.
6. Select enabled entries from `resolved.extensions` and map their paths and metadata to Desktop display records.

This delegates npm, git, URL, local path, legacy global npm fallback, manifest and convention discovery, glob handling, package filters, project-over-user package deduplication, and path safety to Pi. It also includes every extension source represented by Pi's resolver: project explicit entries, project auto-discovery, user explicit entries, user auto-discovery, and package entries, in Pi's precedence order.

The current `extension-package-utils.js` will be reduced to presentation-only helpers. It may derive a stable display name and source label from a resolved resource, but it will not parse package sources, inspect manifests, calculate install paths, or reimplement filter semantics.

Project files are eligible only when `ctx.isProjectTrusted()` is true. The design does not read `trust.json`; this preserves session-only trust decisions and prevents Desktop from independently bypassing Pi's active trust state.

`refreshExtensions({ force = false })` distinguishes a refresh request from an actual resolver run. A normal request for a key with an in-flight or already queued forced run returns the latest scheduled promise and does not allocate another generation. With neither present, it starts one immediately. A forced request always schedules one actual resolver run after the current run, if any; forced requests received before that queued run starts share it. This prevents package-install completion from reusing a resolver snapshot that began before installation while still deduplicating ordinary window-open and session notifications.

The generation for a context key increments only when an actual resolver run starts. Starting a run marks that key as loading, clears its previous view error, and retains its last successful list. Only that key's latest generation may commit. A successful current generation atomically replaces the cached list, clears its error, and ends loading. A failed current generation retains the last successful list for that key, stores only a sanitized error, and ends loading. A stale generation does not mutate the cache or window. A result is sent to the window only if the active context still has the same key; an old `cwd` or trust state is discarded.

The window opens immediately with a loading state when no snapshot exists, then receives `update-skills` after the first successful refresh. Window-open and session notifications use a normal refresh. User-triggered refresh and package-install completion use `force: true` and await the forced run before replying. Session transitions capture a new key, start its refresh, and never send an old-key result.

If settings or package resolution fails, Desktop keeps the last successfully resolved list for the exact `(cwd, projectTrusted)` cache key and returns a sanitized view error. A trusted result is never reused for an untrusted context. Desktop will not fabricate extension entries from partially parsed source strings.

## Model Capability Presets

Model guidance is split into two data sets.

The capability preset contains model facts that are independent of the configured provider transport:

- reasoning support
- text/image input support
- context window
- maximum output tokens

The GPT-5.6 family capability recommendation uses the direct OpenAI model metadata currently shipped with Pi: a 1,050,000-token context window, 128,000 maximum output tokens, image input, and reasoning enabled. The base `gpt-5.6` alias and Sol, Terra, and Luna variants are recognized. Context and output limits are recommendations for custom gateways, not facts about the gateway's enforced limits.

The transport recommendation contains the Pi-to-provider effort mapping. It is selected using the provider API and existing explicit compatibility settings, never the model ID alone.

For the built-in OpenAI provider, or a provider using `https://api.openai.com/v1` with no conflicting explicit `thinkingFormat`, Desktop selects a mapping by API transport. Direct `openai-responses` uses:

| Pi level | Provider effort | Desktop behavior |
| --- | --- | --- |
| `off` | `none` | Available and sent explicitly |
| `minimal` | `null` | Hidden because GPT-5.6 does not document `minimal` |
| `low` | `low` | Available |
| `medium` | `medium` | Available |
| `high` | `high` | Available |
| `xhigh` | `max` | Available and labelled `max` in Desktop |

Mapping Pi `xhigh` to provider `max` is the selected product behavior. Pi Core has no additional slot for the provider's official `xhigh`, so Desktop does not expose both `xhigh` and `max` simultaneously. Desktop describes this as a Desktop mapping, not current Pi registry metadata.

Direct `openai-completions` uses the narrower mapping supported by the current Chat Completions client contract:

| Pi level | Provider effort | Desktop behavior |
| --- | --- | --- |
| `off` | `none` | Available and sent explicitly |
| `minimal` | `null` | Hidden because GPT-5.6 does not document `minimal` |
| `low` | `low` | Available |
| `medium` | `medium` | Available |
| `high` | `high` | Available |
| `xhigh` | `xhigh` | Available |

Desktop does not send `max` through Chat Completions because the installed OpenAI client contract does not currently include it for `reasoning_effort`. That transport may adopt `max` only after both the official Chat Completions contract and the installed client version support it. [OpenAI's GPT-5.6 guidance](https://developers.openai.com/api/docs/guides/latest-model.md) recommends Responses API for reasoning workflows, but Desktop does not silently switch an existing provider's API.

An explicit non-OpenAI `compat.thinkingFormat` and existing `thinkingLevelMap` are preserved and suppress automatic transport mapping. A custom gateway with unknown behavior receives no provider effort mapping. For a recognized reasoning model on such a gateway, Desktop offers a separate explicit action that applies the conservative availability guard `{ off: null, minimal: null, xhigh: null }`, exposing only `low`, `medium`, and `high` without sending an unverified off, minimal, or top-tier value.

Capability application is always explicit. Fetching model IDs only adds neutral model rows and marks recognized recommendations; it does not silently overwrite gateway limits or transport settings. Applying a capability recommendation replaces only `reasoning`, `input`, `contextWindow`, and `maxTokens`, while preserving identity, name, cost, `thinkingLevelMap`, `compat`, and unrelated metadata. Transport application is a separate explicit action: it applies the API-specific Responses or Chat Completions table only to an unambiguous direct OpenAI configuration, or applies the conservative availability guard to an unknown custom gateway.

## Desktop Interaction

Typing a model ID will update only the draft. Every model row always renders a dedicated recommendation container, and the provider editor always renders a dedicated provider-level recommendation container. The `input` event updates the existing model object and patches only that row's container and the provider-level container; it does not redraw `messagesEl`, replace the input node, replace the model object, or replace the enclosing `<details>`. Once the ID matches a known model, the row container shows the capability recommendation and, when available, a separate transport mapping recommendation.

The provider-level "Apply capability recommendations" action will apply recognized capability fields to all matching models. It will only be shown when at least one matching model differs in `reasoning`, `input`, `contextWindow`, or `maxTokens`. Transport mapping differences are displayed and applied separately. After application, the UI shows the effective values and does not continue to offer a no-op action.

The generic "reasoning" checkbox remains available for unknown models. Known-model guidance will not silently change the provider API, `compat`, or `thinkingLevelMap`.

The footer keeps Pi's control value separate from its provider-facing label. Backend snapshots and events send `thinkingLevel`, which is always a Pi `ThinkingLevel`; `thinkingDisplayLevel`, derived from the active model's string `thinkingLevelMap` entry and falling back to the Pi level when no string mapping exists; and `thinkingLevels`, calculated with Pi's exported `getSupportedThinkingLevels(activeModel)`. The click handler finds the next value only in `thinkingLevels`, never in a hard-coded six-level list, while rendering uses `thinkingDisplayLevel`. Model selection, thinking-level selection, provider-request synchronization, and session snapshots recompute all three values, so a direct Responses model keeps internal `xhigh` while displaying `max`, and subsequent clicks continue from `xhigh` correctly.

When `thinkingLevels` contains only `off`, the footer control is visibly disabled, exposes an accessible/title state that the active model does not support configurable thinking, and does not advertise click-to-cycle behavior. A null mapping is never displayed as an active level because Pi excludes that level; if stale state temporarily contains one, Desktop displays the raw Pi level until the backend echo supplies the clamped value.

## Local Configuration

The existing backup `models.json.bak-20260714-003007` proves that the `lookfor-gpt` GPT-5.6 Sol, Terra, and Luna entries previously contained only `id`. Therefore the current `reasoning`, `input`, `contextWindow`, `maxTokens`, `thinkingLevelMap`, and `compat` values are treated as fields injected by the superseded implementation, not as user-authored provider configuration.

Local remediation will create another timestamped backup and restore the three entries to their pre-change shape before setting `reasoning: true` and `thinkingLevelMap: { off: null, minimal: null, xhigh: null }`. This exposes only `low`, `medium`, and `high`, avoiding a false `off`, undocumented `minimal`, and unverified top-tier effort while the `lookfor-gpt` contract is unknown. `input`, `contextWindow`, `maxTokens`, and `compat` remain absent until that gateway's contract is verified or the user explicitly applies a recommendation. Provider API, base URL, credentials, costs, and unrelated models remain unchanged.

## Testing

Regression tests will cover:

- an adapter contract test proving Desktop constructs `SettingsManager` and `DefaultPackageManager` with the captured `cwd` and trust state, passes `onMissing: "skip"`, and consumes resolver output instead of independently parsing sources
- enabled filtering, metadata presentation, and trusted project resources included only when `ctx.isProjectTrusted()` is true
- presentation names for package, explicit, and auto-discovered resolved resources
- settings errors from `drainErrors()` and resolver failures retaining only the last successful list for the same `(cwd, projectTrusted)` key and producing a sanitized error
- generation ordering, normal same-key in-flight sharing, forced-run queuing and coalescing, stale-context result suppression, and trusted-cache isolation
- package installation completing while a pre-install refresh is running, proving the completion response awaits a post-install resolver run
- session and trust-key changes during refresh, proving old-key results cannot mutate or update the active view
- one integration smoke test using the real `DefaultPackageManager`; Desktop tests will not duplicate Pi's full precedence, glob, or legacy npm fallback matrix
- unknown and known model IDs updating without replacing the active input or redrawing the provider view
- continuous entry of a complete known model ID while preserving value, focus, selection, and the model row's expanded state
- GPT-5.6 alias and variant recognition
- capability recommendations that never inject `compat` or `thinkingLevelMap`
- explicit capability application replacing only capability-owned fields while preserving cost, `compat`, and `thinkingLevelMap`
- the exact API-specific direct OpenAI Responses and Chat Completions mapping tables and the conservative custom-gateway guard
- applied/pending state based on the exact fields owned by each recommendation
- provider-level recommendation visibility updating while a model ID is typed without replacing the active model row
- footer raw/display/available-level separation, including skipping null-mapped levels, `xhigh` displayed as `max`, continuous cycling from raw `xhigh`, disabled behavior for an `off`-only model, and recomputation after model changes
- local remediation removing only fields demonstrated by the pre-change backup

The model editor DOM behavior will be extracted into a focused browser module and tested with a `jsdom` harness. The test asserts node identity, `document.activeElement`, `selectionStart`, `selectionEnd`, and `<details>.open` while typing a recognized ID. `jsdom` will be added as an exact development dependency; Playwright is not required for this non-visual interaction regression.

The focused regression scripts will be run red before implementation and green afterward. Final verification is `npm run check`, followed by a fresh diff review.

## Non-Goals

- Changing Pi Core APIs or package-manager behavior
- Adding new reasoning levels to Pi's core `ThinkingLevel` type
- Automatically switching a custom provider from Chat Completions to Responses
- Inferring custom gateway token limits or thinking protocol from an OpenAI model ID
- Modifying unrelated Host UI bridge work
