Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
ea76d5b
fix(stt): run transcription as a server-side job any browser on the s…
andrebrait Oct 3, 2026
45b346e
fix(stt): let a follower stand down when a job it adopted disappears
kahme247 Oct 3, 2026
892399b
feat: add a Slow toggle and provider-neutral usage-limit badge
andrebrait Oct 3, 2026
1a3108c
fix(queue): withdraw the queue through omp's atomic Esc on Stop
andrebrait Oct 3, 2026
f8c875c
fix(queue): fence, retry and gate the atomic Stop fallback
andrebrait Oct 3, 2026
c141485
feat(settings): add Auto thinking source selector
andrebrait Oct 3, 2026
0303849
fix(client): avoid AbortSignal.timeout/any and Promise.withResolvers …
lalondesteve Oct 3, 2026
3cd7b3f
feat(usage): add manual provider usage refresh
andrebrait Oct 4, 2026
036238c
fix(usage): invalidate persisted reports on manual refresh
andrebrait Oct 4, 2026
b2adda8
fix(usage): run queued refresh after failed background fetch
andrebrait Oct 4, 2026
4be8667
feat(composer): show reasoning as TUI glyphs; offer Slow in new sessions
andrebrait Oct 4, 2026
3531a17
feat(composer): show Slow before omp runs
andrebrait Oct 4, 2026
10bab0e
fix: avoid AbortSignal.timeout/any and Promise.withResolvers in clien…
claude Oct 4, 2026
f32b6f2
fix(rpc): follow omp's open-elsewhere session move instead of forking…
claude Oct 4, 2026
12eb02f
feat: generate session titles with omp; show provider accounts
claude Oct 4, 2026
d5b3541
Merge pull request #193 from andrebrait/fix/stop-atomic-esc
kahme247 Oct 4, 2026
f096352
Merge pull request #194 from andrebrait/feat/slow-mode-toggle
kahme247 Oct 4, 2026
cd0bf66
Merge pull request #195 from andrebrait/feat/auto-thinking-source
kahme247 Oct 4, 2026
dab8c82
Merge pull request #201 from andrebrait/feat/provider-usage-refresh
kahme247 Oct 4, 2026
d1fdccb
Merge pull request #199 from lalondesteve/fix/196-browser-api-compati…
kahme247 Oct 4, 2026
971175a
Merge origin/main into claude/practical-archimedes-297gxo
claude Oct 4, 2026
38496eb
test(rpc): restore the closing brace lost in the main merge
claude Oct 4, 2026
c4f89b6
Merge pull request #207 from kahme247/claude/practical-archimedes-297gxo
kahme247 Oct 4, 2026
516c44b
fix: allow private-network and configurable development origins
lalondesteve Oct 3, 2026
3ecbcdf
fix(sidebar): keep workspace names visible beside long branches
lalondesteve Oct 4, 2026
1622a29
fix: support app installation behind authentication
lalondesteve Oct 4, 2026
6250641
ci: rerun verification
lalondesteve Oct 4, 2026
193047f
fix(ui): move session information to the top-right bar
andrebrait Oct 5, 2026
631be73
feat: show skill startup diagnostics
andrebrait Oct 3, 2026
cb98771
feat: dismiss skill startup notices for the session
andrebrait Oct 4, 2026
9fc786c
docs: say the dismissed skill notice returns when its report changes
andrebrait Oct 4, 2026
e92e38f
fix(ui): align session info sizing and remove obsolete composer asser…
andrebrait Oct 5, 2026
38c2104
fix: preserve current state and test boundaries after upstream rebase
andrebrait Oct 5, 2026
adc77f3
fix(ui): restore focus around the session information panel
andrebrait Oct 5, 2026
f6dafb1
fix(composer): size the reasoning glyph to match the toolbar icons
andrebrait Oct 4, 2026
8339320
fix(composer): bump the reasoning glyphs on the trigger and in the menu
andrebrait Oct 4, 2026
fa90af3
fix(composer): bump the reasoning glyphs on the trigger and in the menu
andrebrait Oct 4, 2026
225c8de
fix(composer): scope the big reasoning glyph to narrow toolbars
andrebrait Oct 4, 2026
e92582f
fix(composer): keep the big menu glyphs to narrow toolbars
andrebrait Oct 5, 2026
67043b4
fix(composer): normalize effort icon geometry and alignment
andrebrait Oct 5, 2026
5639893
fix(ui): limit header session information to mobile
andrebrait Oct 5, 2026
3bc67b6
fix(ui): full-screen mobile drawer, flush panel toggle, centered titl…
andrebrait Oct 5, 2026
4224003
feat(nav): add in-app navigate back/forward
tnbao Oct 5, 2026
0a367f6
fix(composer): preserve narrow layouts and custom effort labels
andrebrait Oct 5, 2026
c596539
fix(ui): shorten message actions for mobile
andrebrait Oct 5, 2026
5450ccc
fix(ui): preserve header sizing and title measurement across preferences
andrebrait Oct 5, 2026
9dbf567
fix(ui): compact skill warning controls
andrebrait Oct 5, 2026
f61fb78
fix(composer): keep effort menu levels in full-width rows
andrebrait Oct 5, 2026
fa2d4f9
fix(composer): balance mobile effort and Slow icons
andrebrait Oct 5, 2026
995d948
fix(ui): shorten visible read-aloud action
andrebrait Oct 5, 2026
6aa6d9a
fix(ui): unify reasoning menu rows, size context popover to room, sho…
andrebrait Oct 5, 2026
444e413
fix(ui): restore inline skill warning dismiss button
andrebrait Oct 5, 2026
da7eb34
feat(composer): send images and text files with steers and follow-ups
andrebrait Oct 5, 2026
453ea58
feat(composer): give queued images back on Stop, Edit and refused que…
andrebrait Oct 5, 2026
94a7f7b
fix(composer): recover a refused queued message whole into its own draft
andrebrait Oct 5, 2026
40683b2
fix(composer): keep literal [Image] text and every recovered image
andrebrait Oct 5, 2026
b1311d2
fix(ui): address review — shared picker-row class, boundary-aware pop…
andrebrait Oct 5, 2026
4f5967a
fix(ui): close the session information popover on Escape before run s…
andrebrait Oct 5, 2026
cace683
fix: separate header opener from file-panel close control
andrebrait Oct 5, 2026
4e47c9e
fix: continue mobile panel divider below close button
andrebrait Oct 5, 2026
6e772b8
feat: add mobile sidebar swipes and content-sized titles
andrebrait Oct 5, 2026
5ae8c84
fix: keep drawer gestures behind composer popovers
andrebrait Oct 5, 2026
73aa57f
fix: allow sidebar swipes over owned file collections
andrebrait Oct 5, 2026
b447af0
fix(composer): right-align the controls after the model picker
andrebrait Oct 5, 2026
68846b1
fix: recognize deliberate sidebar swipes away from device edges
andrebrait Oct 5, 2026
fb5f536
fix: tolerate natural curved mobile sidebar swipes
andrebrait Oct 5, 2026
7b8fa05
Merge branch 'pr-212' into scratch
claude Oct 5, 2026
68c75ea
Merge branch 'pr-210' into scratch
claude Oct 5, 2026
bfa0ebc
Merge branch 'pr-214' into scratch
claude Oct 5, 2026
dbc86e5
Merge branch 'pr-211' into scratch
claude Oct 5, 2026
a72d749
Merge branch 'pr-206' into scratch
claude Oct 5, 2026
60de90c
Merge branch 'pr-204' into scratch
claude Oct 5, 2026
28e66a1
Merge branch 'pr-213' into scratch
claude Oct 5, 2026
27d0121
Merge branch 'pr-208' into scratch
claude Oct 5, 2026
104322f
Merge branch 'pr-200' into scratch
claude Oct 5, 2026
9729c99
Merge pull request #215 from kahme247/claude/practical-archimedes-297gxo
kahme247 Oct 5, 2026
383b07e
feat: use hostname-specific installation names and titles
lalondesteve Oct 3, 2026
6c86c13
fix(install-name): fail closed on unparseable Host and localhost subd…
KAHME248 Oct 6, 2026
17ade3a
docs: notifications design spec
andrebrait Oct 6, 2026
8962653
feat: desktop and mobile notifications with Web Push
andrebrait Oct 6, 2026
93e6b82
fix(notifications): address review findings
andrebrait Oct 6, 2026
9d5e854
test(notifications): skip POSIX mode check on Windows
andrebrait Oct 6, 2026
2e5098d
fix(notifications): address Codex and CodeRabbit review
andrebrait Oct 6, 2026
6ca2871
fix(notifications): notify failed prompt responses; honor moved-sessi…
andrebrait Oct 6, 2026
1cb4641
fix(notifications): drop out-of-order presence reports; serialize pre…
andrebrait Oct 6, 2026
02891b8
fix(notifications): resync saved prefs on load even when disabled
andrebrait Oct 6, 2026
5aefbb7
fix(notifications): clear moved-session aliases on a real switch; sho…
andrebrait Oct 6, 2026
02ab6e6
fix(notifications): dim notification settings while notifications are…
andrebrait Oct 7, 2026
b2b1afd
feat(notifications): open the session from a click anywhere on its toast
andrebrait Oct 7, 2026
77cbc25
fix(notifications): ignore drags and pressed controls when a toast ca…
andrebrait Oct 7, 2026
a5899f0
fix(notifications): close the overlaid file panel when a notification…
andrebrait Oct 7, 2026
f0f9ab3
test(notifications): find each notification toast by its title
andrebrait Oct 7, 2026
03c88f7
test(notifications): load the toast module the hook uses, so Windows …
andrebrait Oct 7, 2026
4955035
fix(notifications): clip long session names in pushes and resubscribe…
andrebrait Oct 7, 2026
8c66efd
fix(notifications): a drag brought back to its start does not open th…
andrebrait Oct 7, 2026
8f99c19
feat(notifications): drop the Open link; the toast card itself opens …
andrebrait Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 178 additions & 16 deletions AGENTS.md

Large diffs are not rendered by default.

32 changes: 28 additions & 4 deletions CHANGELOG.md

Large diffs are not rendered by default.

71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,60 @@ Queue **Delete** and **Edit** additionally require `remove_queued_message`. Dele

The queue panel shows omp's own queue (`queuedMessages` in `get_state` and `queue_update` events, omp 18.4.4 or later), so every device viewing a session sees the same queued messages. **Stop** moves the text of messages still waiting in the queue back into the composer instead of letting the agent run them; a steer the model already picked up through live steering still runs. Older omp runtimes show no queue panel, and Stop cannot take queued messages back.

## App installation behind authentication

To install the web app, sign in first and use your browser's installation menu.
The single manifest link requests `/api/manifest` with credentials; that endpoint
uses the existing web-password guard and private, revalidating caching. Its
192×192 and 512×512 PNG icons are embedded from the packaged assets because
Android's native installer fetches ordinary icon URLs without authentication
cookies. The app still launches at `/` with scope `/`.

Keep Cloudflare Access and application authentication enabled; no public
manifest exception or Access bypass rule is needed. This does not add offline
support. Local Chromium verification covered cookie-gated metadata with HTTP
icon URLs blocked (zero installability errors). The owner also confirmed that
installation on a physical phone works as expected behind Cloudflare Access.

Skill startup notices require an OMP runtime with `get_skill_diagnostics`,
`set_skill_startup_diagnostics`, and `skill_diagnostics_update`. Conflicts and
redundant installations appear above the composer when its OMP session starts;
an empty new-chat page does not start OMP just for diagnostics. **Details** shows
the resolved default, variants, identical copies, backing paths, sources, and
selection reason. The **×** button dismisses the notice for that session until
the diagnostic report changes. **Turn off** and **Settings →
Interface & Behavior → Skill startup notices** use OMP's persisted
`skills.showStartupDiagnostics` preference, not a separate browser setting.
**Settings → Extensions & Tools → Skills → View skill diagnostics** remains
available for a selected running session when notices are off. Inspection does
not resume stopped sessions. Missing support or no running session is unavailable,
not a clean result.

## Notifications

Turn on notifications in **Settings → Notifications**. Each browser keeps its own
settings: which events notify (task finished, waiting for input, run failed,
model switched automatically) and what happens while you use omp-web in another
tab (an in-app toast, or always a system notification). Nothing is shown for the
session you are viewing. Clicking a notification opens its session.

- **Push** reaches the browser even when no omp-web tab is open. It needs a
secure context (HTTPS through your reverse proxy, or `localhost`), and the
omp-web server needs outbound HTTPS access to the browser vendors' push
services. omp-web generates its push (VAPID) keys on first use and stores them
with the device list in `~/.omp/agent/omp-web/notifications.json` (mode 0600).
- On **iPhone and iPad** (iOS 16.4 or later), push works only in the app added
to the Home Screen.
- In a secure context without push support, system notifications appear while
an omp-web tab is open. Over plain HTTP on a non-localhost address, browsers
allow no system notifications: only the in-app toasts shown while you use
omp-web remain.
- Desktop browsers show notifications through the operating system's
notification center. Push delivery needs the browser process running.

Completion notifications use omp's `prompt_result` and `session_settled` RPC
frames (omp 18.3.1 or later).

## Quick Start

**Run directly without installing:**
Expand Down Expand Up @@ -212,7 +266,8 @@ host (KDE Plasma, and most Wayland/X11 desktops).
- **Windows System Tray & Service**: Background service, tray icon, logon autostart, and Desktop/Start Menu shortcuts (Windows).
- **macOS launchd Service**: LaunchAgent that starts at login, restarts on crash, and logs under `~/Library/Logs/ompweb`.
- **Linux systemd Service & Tray**: User service that starts at login and restarts on crash, plus a StatusNotifierItem tray icon with service controls (KDE Plasma and compatible desktops).
- **Web-based Settings** (8 tabs): Interface & Behavior, Safety & Approvals, AI Model Defaults, API Keys & Providers, Usage, Agent & Intelligence (advisor, memory, compaction), Agents, Extensions & Tools (MCP, skills, plugins), System & Updates.
- **Notifications**: Desktop and mobile (PWA) notifications for finished tasks, questions waiting for an answer, failed runs, and automatic model switches, with Web Push when served over HTTPS. See [Notifications](#notifications).
- **Web-based Settings** (10 tabs): Interface & Behavior, Safety & Approvals, AI Model Defaults, API Keys & Providers, Usage, Agent & Intelligence (advisor, memory, compaction), Agents, Extensions & Tools (MCP, skills, plugins), Notifications, System & Updates.
- **Slash Commands & Shortcuts**: Quick prompts (`/plan`, `/review`, `/fix`, `/test`, etc.), `⌘K` / `Ctrl+K` palette, and model/reasoning cycling.
- **UI Themes & Localization**: Warm paper light/dark themes plus an omp.sh-inspired midnight (`omp`) theme, chat font size & interface scale, with full English, Chinese (简体中文), and Japanese (日本語) translations.

Expand All @@ -226,6 +281,7 @@ host (KDE Plasma, and most Wayland/X11 desktops).
| `OMP_WEB_NO_OPEN` | Set to `1` to prevent auto-opening browser | `0` |
| `OMP_WEB_DISABLE_AUTOUPDATE` | Set to `1` to disable update checks and in-app updates; restart after changing | `0` |
| `OMP_WEB_OMP_BIN` | Path to `omp` binary if not on `PATH` | _auto-detected_ |
| `OMP_WEB_DEV_ORIGIN` | Additional allowed hostname for the development server (no scheme or port); ignored in production | _None_ |
| `PI_CODING_AGENT_DIR` | Custom omp agent directory | `~/.omp/agent` |
| `OMP_WEB_STT_ENDPOINT` | OpenAI-compatible transcription endpoint URL | _None (disabled)_ |
| `OMP_WEB_STT_KEY` | Optional API key for the STT endpoint | _None_ |
Expand All @@ -242,6 +298,19 @@ npm run dev

The dev server runs at [http://127.0.0.1:30178](http://127.0.0.1:30178).

The development server allows loopback and RFC1918 private IPv4 origins
(`10.0.0.0/8`, `172.16.0.0/12`, and `192.168.0.0/16`). When using a tunnel
or reverse proxy with a custom hostname, set it without editing `next.config.ts`:

```bash
OMP_WEB_DEV_ORIGIN=dev.example.com npm run dev
```

For a persistent setup, set the variable in your local environment or service
configuration and restart the dev server. This does not change the bind address
or enable authentication. Next.js hostname patterns cannot express IPv6 CIDRs;
a private IPv6 origin must be supplied explicitly (for example, `[fd00::1]`).

### Checks

```bash
Expand Down
16 changes: 15 additions & 1 deletion app/api/agent/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ const NO_SPAWN_REPLIES: Record<string, unknown> = {
btw_cancel: { cancelled: false },
};

// Live diagnostics of a skill-resolving omp. A saved or exited session has none,
// and resuming a full child (MCP, LSP) just to answer a read-only inspection or
// flip one setting is not what the click asked for: with no live wrapper these
// answer 409 instead of starting omp, and a live idle child is never replaced
// because the advisor flag differs.
const LIVE_SESSION_ONLY = new Set(["get_skill_diagnostics", "set_skill_startup_diagnostics"]);

/** omp-web's own failures carry a stable code the client can localize; omp's
* errors stay opaque English text. */
function commandErrorResponse(error: unknown) {
Expand Down Expand Up @@ -60,14 +67,21 @@ export async function POST(
// running and pick the flag up at the next natural respawn.
const existing = getRpcSession(id);
const noSpawn = Object.hasOwn(NO_SPAWN_REPLIES, body.type);
const liveOnly = LIVE_SESSION_ONLY.has(body.type);
if (existing?.isAlive()) {
if (noSpawn || existing.advisorSpawned === advisor || existing.isRunning()) {
if (noSpawn || liveOnly || existing.advisorSpawned === advisor || existing.isRunning()) {
const result = await existing.send(body);
return NextResponse.json({ success: true, data: result });
}
await existing.destroyAndWait();
}
if (noSpawn) return NextResponse.json({ success: true, data: NO_SPAWN_REPLIES[body.type] });
if (liveOnly) {
return NextResponse.json(
{ error: "Skill diagnostics require a running session", code: "skill_diagnostics_unavailable" },
{ status: 409 },
);
}

const resolved = await resolveSessionPathOr404(id);
if ("response" in resolved) return resolved.response;
Expand Down
14 changes: 13 additions & 1 deletion app/api/agent/running/events/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { getExitedRpcSessions, getRunningRpcSessions, subscribeRunningSessions } from "@/lib/rpc-manager";
import { isValidNotificationId } from "@/lib/notification-events";
import { attachNotificationClient } from "@/lib/notification-hub";
import { subscribeSessionFileChanges } from "@/lib/session-watcher";

export const dynamic = "force-dynamic";

// GET /api/agent/running/events - SSE stream of the set of currently-running
// session ids. Also carries refresh hints when a live session's file metadata
// changes, so the sidebar can show a newly-started session immediately.
// changes, so the sidebar can show a newly-started session immediately, and
// this tab's notifications when `clientId` is given (one stream per tab:
// browsers allow only six HTTP/1.1 connections per host).
export async function GET(req: Request) {
const clientId = new URL(req.url).searchParams.get("clientId");
// Hoisted so the stream's cancel() (half-open disconnects that never fire
// the abort signal) can release the heartbeat and the subscriber.
let streamCleanup: (() => void) | null = null;
Expand All @@ -18,6 +23,7 @@ export async function GET(req: Request) {
let cleaned = false;
let unsubscribeRunning: (() => void) | null = null;
let unsubscribeFiles: (() => void) | null = null;
let detachNotifications: (() => void) | null = null;
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;

const cleanup = () => {
Expand All @@ -36,6 +42,8 @@ export async function GET(req: Request) {
try { unsubscribeFiles(); } catch {}
unsubscribeFiles = null;
}
detachNotifications?.();
detachNotifications = null;
req.signal?.removeEventListener("abort", cleanup);
try {
controller.close();
Expand Down Expand Up @@ -77,6 +85,10 @@ export async function GET(req: Request) {
encode({ type: "sessions-changed", sessionIds, refreshSessionList: true });
});

if (isValidNotificationId(clientId)) {
detachNotifications = attachNotificationClient(clientId, (message) => encode({ type: "notification", ...message }));
}

// Initial snapshot so the client renders the correct state immediately.
const initialRunning = getRunningRpcSessions();
encode({
Expand Down
33 changes: 33 additions & 0 deletions app/api/manifest/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { MetadataRoute } from "next";
import { getInstallName } from "@/lib/install-name";

// Packaged public assets are read/encoded once per server module. Native
// installers fetch HTTP icons without cookies, even with a credentialed manifest.
const pngDataUrl = (filename: string) =>
`data:image/png;base64,${readFileSync(join(process.cwd(), "public", filename)).toString("base64")}`;

const manifest: MetadataRoute.Manifest = {
description: "Web UI for the oh-my-pi (omp) coding agent",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#000000",
theme_color: "#000000",
icons: [
{ src: pngDataUrl("icon-192.png"), sizes: "192x192", type: "image/png" },
{ src: pngDataUrl("icon.png"), sizes: "512x512", type: "image/png" },
],
};

// Authentication is enforced by the existing /api/ web-password proxy guard.
export function GET(request: Request) {
const name = getInstallName(request.headers);
return Response.json({ ...manifest, name, short_name: name }, {
headers: {
"Content-Type": "application/manifest+json",
"Cache-Control": "private, no-cache, max-age=0, must-revalidate",
},
});
}
15 changes: 12 additions & 3 deletions app/api/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { invalidateModelsCache, loadModelsWithCache, withModelRuntimeError, with
import { disposeUtilityRpc, runUtilityCommand, type OmpModel } from "@/lib/omp/rpc-utility";
import { getModelsConfigPath } from "@/lib/omp/paths";
import { readDisabledProviders } from "@/lib/omp/model-roles";
import { readAnthropicSlowMode } from "@/lib/omp/settings-config";

export const dynamic = "force-dynamic";

Expand Down Expand Up @@ -60,6 +61,12 @@ function supportsFastMode(model: OmpModel): boolean {
return model.provider === "anthropic" || model.provider === "openai" || model.provider === "google";
}

// Copy of omp's AgentSession#slowModeTarget for the composer's Slow toggle
// before omp runs: Claude low priority on `anthropic`, else the flex tier of
// the OpenAI/Google service-tier families. Live get_state wins once omp runs.
// ponytail: provider-only; OpenRouter's openai/google ids need omp's model identity.
const SLOW_MODE_PROVIDERS: Record<string, true> = { anthropic: true, openai: true, "openai-codex": true, google: true, "google-vertex": true };

async function loadModels(): Promise<ModelsData> {
const availableResponse = await runUtilityCommand<{ models?: unknown }>(
{ type: "get_available_models" },
Expand All @@ -81,7 +88,7 @@ async function loadModels(): Promise<ModelsData> {
const nameMap = new Map<string, string>();
const thinkingLevels: Record<string, string[]> = {};
const modelList = available
.map((m) => ({ id: m.id, name: m.name, provider: m.provider, thinkingLevels: thinkingLevelsFor(m), supportsFastMode: supportsFastMode(m), contextWindow: m.contextWindow ?? undefined, maxTokens: m.maxTokens ?? undefined }))
.map((m) => ({ id: m.id, name: m.name, provider: m.provider, thinkingLevels: thinkingLevelsFor(m), supportsFastMode: supportsFastMode(m), supportsSlowMode: SLOW_MODE_PROVIDERS[m.provider] === true, contextWindow: m.contextWindow ?? undefined, maxTokens: m.maxTokens ?? undefined }))
.sort(compareModelEntries);
const loginResponse = await runUtilityCommand<{ providers?: unknown }>(
{ type: "get_login_providers" },
Expand Down Expand Up @@ -137,9 +144,11 @@ const EMPTY_MODELS: ModelsData = {

export async function GET() {
refreshModelsIfConfigChanged();
// Outside the cache: `/slow` on a Claude model rewrites config.yml.
const anthropicSlowMode = readAnthropicSlowMode();
try {
return Response.json(await loadModelsWithCache(MODELS_CACHE_KEY, () => loadModels()));
return Response.json({ ...await loadModelsWithCache(MODELS_CACHE_KEY, () => loadModels()), anthropicSlowMode });
} catch {
return Response.json(withSafeModelLoadFailure(EMPTY_MODELS));
return Response.json({ ...withSafeModelLoadFailure(EMPTY_MODELS), anthropicSlowMode });
}
}
25 changes: 25 additions & 0 deletions app/api/notifications/devices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { isValidNotificationId, parseNotificationPrefs } from "@/lib/notification-events";
import { getNotificationDevice, getVapidPublicKey, parsePushSubscription, saveNotificationDevice } from "@/lib/notification-hub";
import { isRecord } from "@/lib/type-guards";

export const dynamic = "force-dynamic";

/** GET /api/notifications/devices?deviceId= — VAPID public key and whether this device has a push subscription. */
export async function GET(req: Request) {
const deviceId = new URL(req.url).searchParams.get("deviceId");
const device = isValidNotificationId(deviceId) ? getNotificationDevice(deviceId) : undefined;
return NextResponse.json({ publicKey: getVapidPublicKey(), subscribed: Boolean(device?.subscription) });
}

/** PUT /api/notifications/devices { deviceId, prefs, subscription? } — an omitted subscription keeps the stored one. */
export async function PUT(req: Request) {
const body: unknown = await req.json().catch(() => null);
if (!isRecord(body) || !isValidNotificationId(body.deviceId)) {
return NextResponse.json({ error: "deviceId is required", code: "invalid_device" }, { status: 400 });
}
const subscription = body.subscription === undefined ? undefined : parsePushSubscription(body.subscription);
if (subscription === null) return NextResponse.json({ error: "Invalid push subscription", code: "invalid_subscription" }, { status: 400 });
const device = saveNotificationDevice(body.deviceId, parseNotificationPrefs(body.prefs), subscription);
return NextResponse.json({ subscribed: Boolean(device.subscription) });
}
20 changes: 20 additions & 0 deletions app/api/notifications/presence/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import { isValidNotificationId } from "@/lib/notification-events";
import { reportPresence } from "@/lib/notification-hub";
import { isRecord } from "@/lib/type-guards";

export const dynamic = "force-dynamic";

/** POST /api/notifications/presence { clientId, deviceId, visible, sessionId, seq } — which session this tab shows. */
export async function POST(req: Request) {
const body: unknown = await req.json().catch(() => null);
if (
!isRecord(body) || !isValidNotificationId(body.clientId) || !isValidNotificationId(body.deviceId)
|| typeof body.visible !== "boolean" || !Number.isSafeInteger(body.seq)
) {
return NextResponse.json({ error: "Invalid presence report", code: "invalid_presence" }, { status: 400 });
}
const sessionId = typeof body.sessionId === "string" && body.sessionId ? body.sessionId : null;
reportPresence({ clientId: body.clientId, deviceId: body.deviceId, visible: body.visible, sessionId, seq: Number(body.seq) });
return new NextResponse(null, { status: 204 });
}
16 changes: 16 additions & 0 deletions app/api/notifications/test/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
import { isValidNotificationId } from "@/lib/notification-events";
import { sendTestPush } from "@/lib/notification-hub";
import { isRecord } from "@/lib/type-guards";

export const dynamic = "force-dynamic";

/** POST /api/notifications/test { deviceId } — one push to this device; 502 when the push service refuses it. */
export async function POST(req: Request) {
const body: unknown = await req.json().catch(() => null);
if (!isRecord(body) || !isValidNotificationId(body.deviceId)) {
return NextResponse.json({ error: "deviceId is required", code: "invalid_device" }, { status: 400 });
}
const result = await sendTestPush(body.deviceId);
return result.ok ? new NextResponse(null, { status: 204 }) : NextResponse.json({ error: result.error, code: "push_failed" }, { status: 502 });
}
2 changes: 1 addition & 1 deletion app/api/provider-usage/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function GET(request: Request) {
}

try {
return NextResponse.json(await getProviderUsage({ provider, modelId }));
return NextResponse.json(await getProviderUsage({ provider, modelId }, searchParams.get("refresh") === "true"));
} catch {
return NextResponse.json(
{ error: "Provider usage is currently unavailable", code: "provider_usage_unavailable" },
Expand Down
Loading
Loading