From 8e7c3c20ed8cf18f292c34b4f72dcca44ab8f28b Mon Sep 17 00:00:00 2001 From: notsapinho Date: Sun, 13 Sep 2026 11:07:22 -0300 Subject: [PATCH 1/6] Fix unavailable default provider selection --- src/App.tsx | 54 +++++++++++++++++++++++++++++------ src/lib/models.test.ts | 15 ++++++++++ src/lib/models.ts | 10 +++++-- src/lib/session.ts | 3 +- src/surfaces/SettingsView.tsx | 2 +- 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0526ce4c..a944c708 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -165,6 +165,7 @@ import { compactHarnessContext, forgetHarnessSession, generateHarnessTitle, + isHarnessAvailable, isLiveHarness, probeHarnessAvailability, refreshHarnessCatalogs, @@ -610,6 +611,10 @@ function titleTabsEqual(a: TitleTab[], b: TitleTab[]): boolean { // Register capabilities before composer hooks choose their discovery strategy. registerBuiltinHarnesses(); +function newAvailableDefaultSession(cwd?: string, runtimeMode?: RuntimeMode) { + return newDefaultSession(cwd, runtimeMode, isHarnessAvailable); +} + export default function App({ windowTransfer = null, resumed = null, @@ -637,7 +642,7 @@ export default function App({ ); const [seed] = useState(() => { const cwd = lastProjectPath() ?? "~"; - const session = newDefaultSession(cwd); + const session = newAvailableDefaultSession(cwd); const tab = newTab(session.id); return { session, tab }; }); @@ -960,7 +965,35 @@ export default function App({ }, [resumed, readProjectReturnMemory]); useEffect(() => { - void probeHarnessAvailability(); + void probeHarnessAvailability().then(() => { + if (windowTransfer || resumed) return; + const fallback = newAvailableDefaultSession( + seed.session.cwd, + seed.session.runtimeMode, + ); + if ( + fallback.harness === seed.session.harness && + fallback.model === seed.session.model + ) { + return; + } + setSessions((prev) => + prev.map((session) => + session.id === seed.session.id && + session.blocks.length === 0 && + session.harness === seed.session.harness && + session.model === seed.session.model + ? { + ...session, + harness: fallback.harness, + model: fallback.model, + modelSettings: fallback.modelSettings, + title: fallback.title, + } + : session, + ), + ); + }); // Only the harnesses already in this window. Probing every installed CLI // at boot left unused agents (especially Pi) running in the background. const harnesses = [ @@ -1575,7 +1608,10 @@ export default function App({ setInboxViewOpen(false); setNotesViewOpen(false); const cwd = active?.cwd ?? sessionDefaults?.cwd ?? projectCwd; - const session = newDefaultSession(cwd, sessionDefaults?.runtimeMode); + const session = newAvailableDefaultSession( + cwd, + sessionDefaults?.runtimeMode, + ); const tab = newTab(session.id); setSessions((prev) => [...prev, session]); appendTab(tab, cwd); @@ -1604,7 +1640,7 @@ export default function App({ : `#${item.number}`; const linkedWorkItem = linkedWorkItemFromInboxItem(item); const session = { - ...newDefaultSession(cwd, sessionDefaults?.runtimeMode), + ...newAvailableDefaultSession(cwd, sessionDefaults?.runtimeMode), title: `${ref} ${item.title}`, inboxCard: inboxComposerCard(item, description), ...(linkedWorkItem ? { linkedWorkItem } : {}), @@ -1660,7 +1696,7 @@ export default function App({ projectCwd; const title = card.title.trim(); const session = { - ...newDefaultSession(cwd, sessionDefaults?.runtimeMode), + ...newAvailableDefaultSession(cwd, sessionDefaults?.runtimeMode), ...(title ? { title } : {}), noteCard: card, }; @@ -1751,7 +1787,7 @@ export default function App({ const onSplit = useCallback( (dir: SplitDir) => { if (!activeTab) return; - const session = newDefaultSession( + const session = newAvailableDefaultSession( sessionDefaults?.cwd ?? projectCwd, sessionDefaults?.runtimeMode, ); @@ -3186,7 +3222,7 @@ export default function App({ ).body : undefined; session = { - ...newDefaultSession(cwd), + ...newAvailableDefaultSession(cwd), title: `Ask ยท ${item.title}`, inboxAsk: { key, @@ -3390,7 +3426,7 @@ export default function App({ replaceTarget, scope: tabCloseScope, createReplacement: (seed) => - newDefaultSession( + newAvailableDefaultSession( seed?.cwd ?? projectCwdRef.current, seed?.runtimeMode, ), @@ -4028,7 +4064,7 @@ export default function App({ if (nextTabs.length === 0) { const fallback = nextSessions[0]; - const session = newDefaultSession("~", fallback?.runtimeMode); + const session = newAvailableDefaultSession("~", fallback?.runtimeMode); const tab = newTab(session.id); nextSessions = [...nextSessions, session]; nextTabs = [tab]; diff --git a/src/lib/models.test.ts b/src/lib/models.test.ts index 2968f191..f94fb382 100644 --- a/src/lib/models.test.ts +++ b/src/lib/models.test.ts @@ -225,6 +225,21 @@ describe("provider defaults", () => { }); }); + it("uses the first installed provider when the preferred one is unavailable", () => { + expect(defaultSessionChoice((harness) => harness === "codex")).toEqual({ + harness: "codex", + model: defaultModelId("codex"), + }); + }); + + it("keeps an installed saved default provider", () => { + saveLastModelChoice("opencode", "opencode:glm-5"); + expect(defaultSessionChoice((harness) => harness === "opencode")).toEqual({ + harness: "opencode", + model: "opencode:glm-5", + }); + }); + it("keeps the six most recently used unique models", () => { saveRecentModelChoice("claude", "claude:opus-5"); saveRecentModelChoice("cursor", "cursor:composer-2.5"); diff --git a/src/lib/models.ts b/src/lib/models.ts index 91a0c1a0..c412ae7a 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -594,9 +594,15 @@ export function preferredModelId(harness: HarnessId): string { } /** Provider + model new conversations should start with. */ -export function defaultSessionChoice(): LastModelChoice { +export function defaultSessionChoice( + available?: (id: HarnessId) => boolean, +): LastModelChoice { const last = loadLastModelChoice(); - const harness = last?.harness ?? "cursor"; + const preferred = last?.harness ?? "cursor"; + const harness = + available && !available(preferred) + ? (HARNESSES.find(available) ?? preferred) + : preferred; return { harness, model: preferredModelId(harness) }; } diff --git a/src/lib/session.ts b/src/lib/session.ts index 947b1ceb..95c8ade2 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -400,8 +400,9 @@ export function newSession( export function newDefaultSession( cwd = "~", runtimeMode: RuntimeMode = DEFAULT_RUNTIME_MODE, + available?: (id: HarnessId) => boolean, ): Session { - const choice = defaultSessionChoice(); + const choice = defaultSessionChoice(available); return newSession(choice.harness, cwd, choice.model, runtimeMode); } diff --git a/src/surfaces/SettingsView.tsx b/src/surfaces/SettingsView.tsx index b1767c1c..d2cbdb66 100644 --- a/src/surfaces/SettingsView.tsx +++ b/src/surfaces/SettingsView.tsx @@ -1718,7 +1718,7 @@ function ProviderRow({ ) : null} current && onDefault(harness, current.id)} - disabled={isDefault || !current} + disabled={isDefault || !available || !current} > {isDefault ? "Default" : "Use by default"} From eebc6ad4bc002d60de88be2abaafd5978a586de7 Mon Sep 17 00:00:00 2001 From: notsapinho Date: Sun, 13 Sep 2026 11:07:23 -0300 Subject: [PATCH 2/6] Harden available provider fallback --- src/App.tsx | 51 +++++++++++++++++------------------ src/lib/session.ts | 3 +-- src/surfaces/SettingsView.tsx | 7 ++++- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a944c708..20d64599 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -217,6 +217,7 @@ import { notifyDirsChanged } from "./lib/fileTree"; import { nudgeWatchedFiles } from "./lib/fileWatch"; import { type EditorNavigationTarget, type OpenFileFn } from "./lib/search"; import { + defaultSessionChoice, mergeModelSettings, preferredModelSettings, resolveModel, @@ -264,7 +265,6 @@ import { canReplaceSessionTitle, formatSessionTitle, sessionNeedsInput, - newDefaultSession, newSession, sessionDisplayTitle, sessionWorkCwd, @@ -612,7 +612,8 @@ function titleTabsEqual(a: TitleTab[], b: TitleTab[]): boolean { registerBuiltinHarnesses(); function newAvailableDefaultSession(cwd?: string, runtimeMode?: RuntimeMode) { - return newDefaultSession(cwd, runtimeMode, isHarnessAvailable); + const { harness, model } = defaultSessionChoice(isHarnessAvailable); + return newSession(harness, cwd, model, runtimeMode); } export default function App({ @@ -967,31 +968,29 @@ export default function App({ useEffect(() => { void probeHarnessAvailability().then(() => { if (windowTransfer || resumed) return; - const fallback = newAvailableDefaultSession( - seed.session.cwd, - seed.session.runtimeMode, - ); - if ( - fallback.harness === seed.session.harness && - fallback.model === seed.session.model - ) { - return; - } setSessions((prev) => - prev.map((session) => - session.id === seed.session.id && - session.blocks.length === 0 && - session.harness === seed.session.harness && - session.model === seed.session.model - ? { - ...session, - harness: fallback.harness, - model: fallback.model, - modelSettings: fallback.modelSettings, - title: fallback.title, - } - : session, - ), + prev.map((session) => { + if ( + session.blocks.length > 0 || + isHarnessAvailable(session.harness) + ) { + return session; + } + const fallback = newAvailableDefaultSession( + session.cwd, + session.runtimeMode, + ); + return { + ...session, + harness: fallback.harness, + model: fallback.model, + modelSettings: fallback.modelSettings, + title: + session.title === HARNESS_LABEL[session.harness] + ? fallback.title + : session.title, + }; + }), ); }); // Only the harnesses already in this window. Probing every installed CLI diff --git a/src/lib/session.ts b/src/lib/session.ts index 95c8ade2..947b1ceb 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -400,9 +400,8 @@ export function newSession( export function newDefaultSession( cwd = "~", runtimeMode: RuntimeMode = DEFAULT_RUNTIME_MODE, - available?: (id: HarnessId) => boolean, ): Session { - const choice = defaultSessionChoice(available); + const choice = defaultSessionChoice(); return newSession(choice.harness, cwd, choice.model, runtimeMode); } diff --git a/src/surfaces/SettingsView.tsx b/src/surfaces/SettingsView.tsx index d2cbdb66..c8a6431d 100644 --- a/src/surfaces/SettingsView.tsx +++ b/src/surfaces/SettingsView.tsx @@ -107,6 +107,7 @@ import { } from "../lib/uiScale"; import { getHarnessAvailabilitySnapshot, + hasProbedHarnessAvailability, harnessUnavailableHint, isHarnessAvailable, probeHarnessAvailability, @@ -114,6 +115,7 @@ import { } from "../lib/harness/availability"; import { refreshHarnessCatalogs } from "../lib/harness/registry"; import { + defaultSessionChoice, defaultModelId, getModelSnapshot, isPickerProviderVisible, @@ -1607,6 +1609,9 @@ function ProvidersPage() { ); const [choice, setChoice] = useState(loadLastModelChoice); const [defaultModels, setDefaultModels] = useState(loadDefaultModels); + const effectiveChoice = hasProbedHarnessAvailability() + ? defaultSessionChoice(isHarnessAvailable) + : choice; useEffect(() => { void probeHarnessAvailability(); @@ -1646,7 +1651,7 @@ function ProvidersPage() { ? choice.model : defaultModelId(harness)) } - isDefault={choice?.harness === harness} + isDefault={effectiveChoice?.harness === harness} onDefault={onDefault} onModelChange={onModelChange} /> From 2d4b1bceaa5bba2fe9c79e94247784b35c05ddee Mon Sep 17 00:00:00 2001 From: Nick <60738984+hardbeat920@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:50:42 +0100 Subject: [PATCH 3/6] Fix duplicate availability import after rebase --- src/App.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index 20d64599..347a8f83 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -165,7 +165,6 @@ import { compactHarnessContext, forgetHarnessSession, generateHarnessTitle, - isHarnessAvailable, isLiveHarness, probeHarnessAvailability, refreshHarnessCatalogs, From 1adab29a27f32d6ff0bf4649ad70520127a06bb6 Mon Sep 17 00:00:00 2001 From: notsapinho Date: Mon, 14 Sep 2026 15:05:51 -0300 Subject: [PATCH 4/6] Preserve available seed session settings --- src/App.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 347a8f83..2d2d28dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2503,12 +2503,15 @@ export default function App({ const seedSession = (cwd: string) => { const seed = sessionsRef.current[0]; + if (!seed || !isHarnessAvailable(seed.harness)) { + return newAvailableDefaultSession(cwd, seed?.runtimeMode); + } return newSession( - seed?.harness ?? "claude", + seed.harness, cwd, - seed?.model, - seed?.runtimeMode, - seed?.modelSettings, + seed.model, + seed.runtimeMode, + seed.modelSettings, ); }; From 771be50cf7d1b51f0639e213810f60d0c0286671 Mon Sep 17 00:00:00 2001 From: notsapinho Date: Mon, 14 Sep 2026 15:12:53 -0300 Subject: [PATCH 5/6] Reuse available seed session selection Share the availability check across replacement-session paths so installed seed providers keep their configuration and unavailable providers fall back safely. --- src/App.tsx | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 2d2d28dc..6068f268 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -615,6 +615,19 @@ function newAvailableDefaultSession(cwd?: string, runtimeMode?: RuntimeMode) { return newSession(harness, cwd, model, runtimeMode); } +function newSessionForSeed(seed: Session | undefined, cwd: string) { + if (!seed || !isHarnessAvailable(seed.harness)) { + return newAvailableDefaultSession(cwd, seed?.runtimeMode); + } + return newSession( + seed.harness, + cwd, + seed.model, + seed.runtimeMode, + seed.modelSettings, + ); +} + export default function App({ windowTransfer = null, resumed = null, @@ -2289,12 +2302,9 @@ export default function App({ return; } const seed = sessionsRef.current[0]; - const session = newSession( - seed?.harness ?? "claude", + const session = newSessionForSeed( + seed, file.cwd || projectCwd, - seed?.model, - seed?.runtimeMode, - seed?.modelSettings, ); setSessions((prev) => [...prev, session]); setTabs((prev) => @@ -2501,20 +2511,6 @@ export default function App({ ); if (!tab) return; - const seedSession = (cwd: string) => { - const seed = sessionsRef.current[0]; - if (!seed || !isHarnessAvailable(seed.harness)) { - return newAvailableDefaultSession(cwd, seed?.runtimeMode); - } - return newSession( - seed.harness, - cwd, - seed.model, - seed.runtimeMode, - seed.modelSettings, - ); - }; - // Stage one: files open in the active tab's editor panes close first. // Only when none are open does the command close every workspace tab. const editorFiles = tab.editorPanes.flatMap((pane) => pane.files); @@ -2546,7 +2542,10 @@ export default function App({ ); } else { // The tab held only editor panes and must stay: seed a session. - const session = seedSession(editorFiles[0].cwd || projectCwd); + const session = newSessionForSeed( + sessionsRef.current[0], + editorFiles[0].cwd || projectCwd, + ); setSessions((prev) => [...prev, session]); nextTab = resetTabToSession(tab, session.id); focusesSession = true; @@ -2620,7 +2619,10 @@ export default function App({ return; } // The tab held no session: seed one so the workspace stays usable. - const session = seedSession(terminalFiles[0]?.cwd || projectCwd); + const session = newSessionForSeed( + sessionsRef.current[0], + terminalFiles[0]?.cwd || projectCwd, + ); setSessions((prev) => [...prev, session]); setTabs((prev) => prev.map((entry) => From 806501ae483457cd2a8d6ac033217176289cd21f Mon Sep 17 00:00:00 2001 From: notsapinho Date: Mon, 14 Sep 2026 20:22:56 -0300 Subject: [PATCH 6/6] Complete availability-aware session replacement Route every replacement flow through the shared seed helper while preserving open-session settings and restored metadata. --- src/App.tsx | 51 ++++++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6068f268..00033c23 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -615,7 +615,14 @@ function newAvailableDefaultSession(cwd?: string, runtimeMode?: RuntimeMode) { return newSession(harness, cwd, model, runtimeMode); } -function newSessionForSeed(seed: Session | undefined, cwd: string) { +type SessionSeed = Pick< + Session | SessionSummary, + "harness" | "model" | "runtimeMode" +> & { + modelSettings?: Session["modelSettings"]; +}; + +function newSessionForSeed(seed: SessionSeed | undefined, cwd: string) { if (!seed || !isHarnessAvailable(seed.harness)) { return newAvailableDefaultSession(cwd, seed?.runtimeMode); } @@ -2461,13 +2468,7 @@ export default function App({ const finishClear = () => { persistSession(oldSession); - const session = newSession( - oldSession.harness, - oldSession.cwd, - oldSession.model, - oldSession.runtimeMode, - oldSession.modelSettings, - ); + const session = newSessionForSeed(oldSession, oldSession.cwd); setSessions((prev) => [...prev, session]); setDirtyFiles((prev) => { @@ -3263,13 +3264,7 @@ export default function App({ ), ); const fresh = { - ...newSession( - current.harness, - current.cwd, - current.model, - current.runtimeMode, - current.modelSettings, - ), + ...newSessionForSeed(current, current.cwd), title: current.title, inboxAsk: current.inboxAsk, }; @@ -3831,13 +3826,7 @@ export default function App({ ) { setProjectCwd(normalized); setRecents(rememberProject(normalized)); - const session = newSession( - current.harness, - normalized, - current.model, - current.runtimeMode, - current.modelSettings, - ); + const session = newSessionForSeed(current, normalized); const tab = newTab(session.id); setSessions((prev) => [...prev, session]); appendTab(tab, normalized); @@ -3955,13 +3944,7 @@ export default function App({ } const seed = current ?? sessionsRef.current[0]; - const session = newSession( - seed?.harness ?? "claude", - normalized, - seed?.model, - seed?.runtimeMode, - seed?.modelSettings, - ); + const session = newSessionForSeed(seed, normalized); const tab = newTab(session.id); setProjectCwd(normalized); setRecents(rememberProject(normalized)); @@ -5701,11 +5684,13 @@ export default function App({ runtimeMode: lead.runtimeMode, } : { - ...newSession( - task.harness, + ...newSessionForSeed( + { + harness: task.harness, + model: task.model, + runtimeMode: lead.runtimeMode, + }, run.cwd, - task.model, - lead.runtimeMode, ), id: task.sessionId, title: task.title,