summaryrefslogtreecommitdiffhomepage
path: root/packages/api/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-02 15:03:54 +0900
committerAdam Malczewski <[email protected]>2026-06-02 15:03:54 +0900
commit9d6b7a97e8e96429815503718e1437fae41bf5d5 (patch)
treeba3b3a95454a6d150e34b595d92d39acacb8ad6a /packages/api/src
parentecb001ec7a2e573d8dedf5064e860e5a3e7788fd (diff)
parent40b0b6a23a5cbd494f9956315c2e424d16edb282 (diff)
downloaddispatch-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.ts64
-rw-r--r--packages/api/src/routes/tabs.ts13
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);