Skip to content

Persist model / reasoning / speed / permission selection across sessions and app returns #37

@Whiteknight07

Description

@Whiteknight07

Summary

Model selection, reasoning effort, speed, and permission mode in the AI panel are not persisted. They live only in component state and reset back to the provider-reported default every time the panel re-initializes (e.g. on app return from background, on session disconnect, on sessionState === "ended" | "expired").

On iOS this is very visible: pick a non-default model (anything other than the GPT-5.5 default), send a message, leave the app, return — and the selector has snapped back to GPT-5.5.

Repro

  1. Open the app, connect a CLI session.
  2. In the AI panel, change model to something other than the backend's default (e.g. for Codex pick a non-default model), set reasoning to medium, set permission mode to full-access.
  3. Send a message — it sends with the chosen options as expected.
  4. Background the app for a moment and reopen (or disconnect/reconnect the session).
  5. Reopen the panel: model has reverted to the provider default; reasoning/speed/permission have reverted to their initial constants.

Expected

  • Once the user has picked model / reasoning / speed / permission, those choices stick — both within an ongoing chat and across app launches — until the user changes them again.
  • Falling back to the provider default should only happen the first time a user touches the backend, or when the previously-selected value is no longer in the provider's supported list.

Where it happens

All state is plain useState (no persistence):

app/plugins/core/ai/Panel.tsx ~ L2715–L2729

const [selectedModelByBackend, setSelectedModelByBackend] = useState<Record<AiBackend, string>>({ opencode: "", codex: "" });
const [codexReasoningEffort, setCodexReasoningEffort] = useState(..."medium");
const [codexSpeed, setCodexSpeed] = useState("default");
const [codexPermissionMode, setCodexPermissionMode] = useState(..."default");

The init effect re-fetches providers and unconditionally overwrites the selection to the backend default, at ~ L3308–L3345:

setSelectedModelByBackend((prev) => ({
  ...prev,
  [backend]: models.length > 0 ? (defaultModelId || models[0].id) : "",
}));

isInitialized is reset on disconnect / session end at ~ L3395–L3400, which causes init to run again and clobber the selection on return.

Only AI_DETAILED_VIEW_STORAGE_KEY is persisted via AsyncStorage today (Panel.tsx L75, L3556, L3568). Nothing else.

Proposed fix (minimal)

Persist the four pieces of selection state via @react-native-async-storage/async-storage (already imported) using keys like:

  • ai-selected-model:<backend>
  • ai-selected-agent:<backend>
  • ai-codex-reasoning
  • ai-codex-speed
  • ai-codex-permission

Then:

  1. Hydrate them on mount before/alongside provider fetch.
  2. Write on every change.
  3. In the provider-fetch block (L3342), prefer the persisted value if it still exists in models, else fall back to defaultModelId || models[0].id.
  4. The existing validators at L2900–L2913 already snap reasoning/speed back to a valid value if a persisted value is no longer supported, so no extra work needed there.

This is a small, contained change inside Panel.tsx. Happy to send a PR if a maintainer can confirm direction.

Scope / non-goals

  • Not adding a settings pane in this issue (that's a follow-up — app/settings/ai.tsx currently only exposes aiFontSize).
  • Not changing the picker UI.
  • Not changing defaults for first-time users.

Environment

  • Reproduced on iPhone app build (latest from App Store as of 2026-05-18).
  • Source inspected at lunel-dev/lunel@main.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions