summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-08-17 15:04:11 +0900
committerAdam Malczewski <[email protected]>2026-08-17 15:04:11 +0900
commit63588c3c69b32d180dc78a2cc187cc8aa22ad7e0 (patch)
treeb0783f6ad41e8eaa81036e250104067964de8024 /src
parent7b9763951cbbaa366b506d1628414c6a883b7d0b (diff)
parentac5a6acddc78d7df3d44bda87fe287bc9391531d (diff)
downloaddispatch-web-63588c3c69b32d180dc78a2cc187cc8aa22ad7e0.tar.gz
dispatch-web-63588c3c69b32d180dc78a2cc187cc8aa22ad7e0.zip
Merge branch 'dev' into feature/numbered-opencode-credentials
Diffstat (limited to 'src')
-rw-r--r--src/app/store.svelte.ts13
-rw-r--r--src/app/store.test.ts35
2 files changed, 48 insertions, 0 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts
index e11a445..9d0e605 100644
--- a/src/app/store.svelte.ts
+++ b/src/app/store.svelte.ts
@@ -779,6 +779,11 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
try {
const res = await fetchImpl(`${httpBase}/conversations/${encodeURIComponent(id)}/thinking`);
if (!res.ok) return;
+ // Guard against the SPA fallback: when DISPATCH_WEB_DIR is set (production),
+ // unknown API routes get index.html with HTTP 200 + text/html instead of a
+ // 404. Treat any non-JSON 200 as "endpoint not shipped" → leave null (ON).
+ const contentType = res.headers.get("content-type") ?? "";
+ if (!contentType.includes("application/json")) return;
const data = (await res.json()) as ThinkingResponse;
// Guard a slow response losing a race with a conversation switch.
if (workspaceConversationId() === id) thinking = data.thinking ?? null;
@@ -1711,6 +1716,14 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
error: errBody?.error ?? `Set thinking failed (HTTP ${res.status})`,
};
}
+ // Guard against the SPA fallback (200 + text/html when the endpoint
+ // is not shipped). Optimistically apply locally — the backend can't
+ // persist it, but the UI reflects the user's intent for this session.
+ const contentType = res.headers.get("content-type") ?? "";
+ if (!contentType.includes("application/json")) {
+ if (workspaceConversationId() === id) thinking = enabled;
+ return { ok: true, thinking: enabled };
+ }
const data = (await res.json()) as ThinkingResponse;
const next = data.thinking ?? enabled;
if (workspaceConversationId() === id) thinking = next;
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
index e172c8f..d30148e 100644
--- a/src/app/store.test.ts
+++ b/src/app/store.test.ts
@@ -957,6 +957,7 @@ describe("createAppStore", () => {
if (url.endsWith("/thinking")) {
return new Response(JSON.stringify({ conversationId: "x", thinking: false }), {
status: 200,
+ headers: { "content-type": "application/json" },
});
}
return base(input, init);
@@ -1005,6 +1006,38 @@ describe("createAppStore", () => {
store.dispose();
});
+ it("treats a SPA-fallback response (200 + text/html) as 'never set' (null ⇒ ON)", async () => {
+ // In production (DISPATCH_WEB_DIR set), unknown API routes get the SPA
+ // fallback: index.html with HTTP 200 + text/html. This must NOT crash
+ // res.json() — it should leave `thinking` null (ON, the default).
+ const base = fakeFetchImpl();
+ const fetchImpl: typeof fetch = async (input, init) => {
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
+ if (url.endsWith("/thinking")) {
+ return new Response("<!DOCTYPE html><html><body>SPA fallback</body></html>", {
+ status: 200,
+ headers: { "content-type": "text/html; charset=utf-8" },
+ });
+ }
+ return base(input, init);
+ };
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl,
+ localStorage: createFakeStorage(),
+ });
+ ws.resolveOpen();
+
+ // give the (HTML) fetch a tick to settle
+ await vi.waitFor(() => {
+ expect(store.reasoningEffort).not.toBe(undefined);
+ });
+ expect(store.thinking).toBeNull();
+
+ store.dispose();
+ });
+
it("setThinking PUTs the flag and updates local state from the echo", async () => {
const calls: { url: string; method: string; body: string | undefined }[] = [];
const base = fakeFetchImpl();
@@ -1015,11 +1048,13 @@ describe("createAppStore", () => {
const sent = JSON.parse(init.body as string) as { thinking: boolean };
return new Response(JSON.stringify({ conversationId: "x", thinking: sent.thinking }), {
status: 200,
+ headers: { "content-type": "application/json" },
});
}
if (url.endsWith("/thinking")) {
return new Response(JSON.stringify({ conversationId: "x", thinking: null }), {
status: 200,
+ headers: { "content-type": "application/json" },
});
}
return base(input, init);