diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 15:03:54 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 15:03:54 +0900 |
| commit | 9d6b7a97e8e96429815503718e1437fae41bf5d5 (patch) | |
| tree | ba3b3a95454a6d150e34b595d92d39acacb8ad6a /packages/api/src | |
| parent | ecb001ec7a2e573d8dedf5064e860e5a3e7788fd (diff) | |
| parent | 40b0b6a23a5cbd494f9956315c2e424d16edb282 (diff) | |
| download | dispatch-9d6b7a97e8e96429815503718e1437fae41bf5d5.tar.gz dispatch-9d6b7a97e8e96429815503718e1437fae41bf5d5.zip | |
Merge branch 'dev' into td/todo-fix
Diffstat (limited to 'packages/api/src')
| -rw-r--r-- | packages/api/src/routes/models.ts | 64 | ||||
| -rw-r--r-- | packages/api/src/routes/tabs.ts | 13 |
2 files changed, 71 insertions, 6 deletions
diff --git a/packages/api/src/routes/models.ts b/packages/api/src/routes/models.ts index 6a0f5dc..8f64bbb 100644 --- a/packages/api/src/routes/models.ts +++ b/packages/api/src/routes/models.ts @@ -1,8 +1,10 @@ +import { randomUUID } from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import type { ModelRegistry } from "@dispatch/core"; import { ANTHROPIC_MODELS_FALLBACK, + buildWakeProbeBody, type ClaudeAccount, fetchAnthropicModels, fetchCopilotUsage, @@ -566,6 +568,39 @@ modelsRoutes.post("/remove-key", async (c) => { // ─── Shared wake function ───────────────────────────────────── +/** + * Model used for the wake probe. A small/cheap model is enough — the only + * purpose is to register activity against the subscription so its rate-limit + * window keeps resetting on schedule. + */ +const WAKE_PROBE_MODEL = "claude-3-5-haiku-20241022"; + +/** Max chars of upstream error body to keep in the surfaced message. */ +const MAX_ERROR_BODY_CHARS = 200; + +/** + * Turn a non-OK probe response into a short, human-readable reason. Anthropic + * returns a JSON error envelope (`{ error: { message } }`); fall back to a + * truncated raw body, then to the bare status. Never throws. + */ +async function describeFailedResponse(res: Response): Promise<string> { + let detail = ""; + try { + const text = await res.text(); + try { + const parsed = JSON.parse(text) as { error?: { message?: unknown } }; + const message = parsed?.error?.message; + detail = typeof message === "string" ? message : text; + } catch { + detail = text; + } + } catch { + detail = ""; + } + detail = detail.trim().slice(0, MAX_ERROR_BODY_CHARS); + return detail ? `HTTP ${res.status}: ${detail}` : `HTTP ${res.status}`; +} + async function wakeAllClaudeAccounts(): Promise< Array<{ label: string; ok: boolean; error?: string }> > { @@ -596,20 +631,37 @@ async function wakeAllClaudeAccounts(): Promise< continue; } + // Mirror a genuine Claude Code CLI request. These are OAuth + // (Pro/Max) subscription accounts: Anthropic validates the + // `system[]` array and rejects (401/403) any request whose system + // block lacks the verbatim Claude Code identity string. A bare + // `{ model, messages }` body — what this probe used to send — + // always failed, which is why scheduled wakes silently died with a + // blank "failed" status. `buildWakeProbeBody` produces the correct + // shape (billing header + identity); the session/request-id headers + // match what the real CLI stamps so the probe isn't flagged. const res = await fetch("https://api.anthropic.com/v1/messages", { method: "POST", headers: { ...getAnthropicHeaders(creds.accessToken), "content-type": "application/json", + "X-Claude-Code-Session-Id": randomUUID(), + "x-client-request-id": randomUUID(), }, - body: JSON.stringify({ - model: "claude-3-5-haiku-20241022", - max_tokens: 16, - messages: [{ role: "user", content: "hi" }], - }), + body: JSON.stringify(buildWakeProbeBody(WAKE_PROBE_MODEL)), }); - results.push({ label: acct.label, ok: res.ok }); + if (res.ok) { + results.push({ label: acct.label, ok: true }); + } else { + // Surface WHY it failed so the panel never shows a bare + // "failed" again and breakage stays debuggable. + results.push({ + label: acct.label, + ok: false, + error: await describeFailedResponse(res), + }); + } } catch (err) { results.push({ label: acct.label, diff --git a/packages/api/src/routes/tabs.ts b/packages/api/src/routes/tabs.ts index f52ee99..28a89f1 100644 --- a/packages/api/src/routes/tabs.ts +++ b/packages/api/src/routes/tabs.ts @@ -11,6 +11,7 @@ import { listOpenTabs, setSetting, updateTabModel, + updateTabPositions, updateTabStatus, updateTabTitle, } from "@dispatch/core"; @@ -63,6 +64,18 @@ tabsRoutes.put("/settings/title-model", async (c) => { return c.json({ success: true }); }); +// Reorder open tabs. Body `{ ids }` is the new left-to-right order of tab ids; +// each tab's `position` is rewritten to its index. Must be declared before the +// `/:id` routes so "reorder" isn't captured as an id param. +tabsRoutes.patch("/reorder", async (c) => { + const body = await c.req.json<{ ids?: string[] }>(); + if (!Array.isArray(body.ids) || body.ids.some((id) => typeof id !== "string")) { + return c.json({ error: "ids must be an array of strings" }, 400); + } + updateTabPositions(body.ids); + return c.json({ success: true }); +}); + tabsRoutes.get("/:id", (c) => { const id = c.req.param("id"); const tab = getTab(id); |
