fix(walkthrough): name an outdated server instead of failing to parse its HTML

A server without these routes does not answer 404 with JSON. The unmatched
/api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI
for anything it does not recognise — HTML, status 200 — so a client newer than
its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype"
in the panel, naming neither the cause nor the remedy.

The client now checks the content type before parsing. A non-JSON answer on 2xx
or 404 blocks with "this server is older than the app, update it and refresh".
A non-JSON 5xx keeps its own failure: a server that answered badly is not a
server missing the feature, and sending that user to upgrade chases the wrong
thing.
This commit is contained in:
Bohdan Triapitsyn
2026-08-04 19:06:19 +03:00
parent bcae0fcfc3
commit 8c37061886
17 changed files with 178 additions and 6 deletions
@@ -6,10 +6,10 @@ import { useI18n } from '@/lib/i18n';
import { useConfigStore } from '@/stores/useConfigStore';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { updateDesktopSettings } from '@/lib/persistence';
import type { WalkthroughBlockedReason, WalkthroughModel } from '@/lib/walkthrough/types';
import type { WalkthroughBlockedState, WalkthroughModel } from '@/lib/walkthrough/types';
interface WalkthroughBlockerProps {
reason: WalkthroughBlockedReason;
reason: WalkthroughBlockedState;
model?: WalkthroughModel;
requiredChars?: number;
availableChars?: number;
@@ -102,6 +102,7 @@ export const WalkthroughBlocker = ({
if (reason === 'no-model') return t('walkthrough.blocked.noModel.description');
if (reason === 'empty-diff') return t('walkthrough.blocked.emptyDiff.description');
if (reason === 'only-generated') return t('walkthrough.blocked.onlyGenerated.description');
if (reason === 'server-unsupported') return t('walkthrough.blocked.serverUnsupported.description');
if (reason === 'output-exhausted') {
return label
? t('walkthrough.blocked.outputExhausted.description', { model: label })
@@ -125,6 +126,7 @@ export const WalkthroughBlocker = ({
if (reason === 'no-model') return t('walkthrough.blocked.noModel.title');
if (reason === 'empty-diff') return t('walkthrough.blocked.emptyDiff.title');
if (reason === 'only-generated') return t('walkthrough.blocked.onlyGenerated.title');
if (reason === 'server-unsupported') return t('walkthrough.blocked.serverUnsupported.title');
if (reason === 'output-exhausted') return t('walkthrough.blocked.outputExhausted.title');
if (reason === 'structured-output-unsupported') return t('walkthrough.blocked.structuredOutput.title');
return t('walkthrough.blocked.contextTooSmall.title');
@@ -156,7 +158,9 @@ export const WalkthroughBlocker = ({
</div>
)}
{(reason === 'empty-diff' || reason === 'only-generated') && (
{/* Retry is the whole remedy once the server is updated, so it stays in
reach rather than sending the user back through the panel header. */}
{(reason === 'empty-diff' || reason === 'only-generated' || reason === 'server-unsupported') && (
<Button type="button" variant="outline" size="sm" onClick={onRetry}>
{t('walkthrough.action.refresh')}
</Button>
@@ -440,6 +440,9 @@ export const WalkthroughView = ({ directory }: WalkthroughViewProps) => {
|| entry.error?.code === 'empty-diff'
|| entry.error?.code === 'only-generated'
|| entry.error?.code === 'output-exhausted'
// Client-detected rather than reported: the server answered something that
// was not JSON, so it has no walkthrough routes at all.
|| entry.error?.code === 'server-unsupported'
? entry.error.code
: entry.readiness && !entry.readiness.ready && !view
&& entry.readiness.reason !== 'no-provider-login'