Merge upstream v0.4.0 into Gitea fork

This commit is contained in:
2026-10-31 13:45:06 +03:00
286 changed files with 24403 additions and 2546 deletions
+3 -1
View File
@@ -1,4 +1,4 @@
import { compactionInFlight, deleteChatSession, getChat, isTurnRunning, sessionCoverage, sessionCost, stores, type ChatDeleteMode } from "@/src/server";
import { compactionInFlight, deleteChatSession, getChat, isTurnRunning, sessionCoverage, sessionCost, stores, turnFailure, type ChatDeleteMode } from "@/src/server";
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
@@ -19,6 +19,8 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
messages: chat.messages,
pricing: sessionCost(chat),
turnInFlight: isTurnRunning(id),
// Also reaches idle views that missed the failing turn's stream.
turnError: turnFailure(id),
compacting: compactionInFlight(id),
coverage: sessionCoverage(id),
});
+4 -3
View File
@@ -5,8 +5,9 @@
* submitter too attaches here. One SSE stream per chat: a running turn →
* subscribe to the live turn object: FULL replay of the event log, then
* live chunks until settle ([DONE]). No running turn → the cheap terminal
* ({"type":"turn-settled"} + [DONE]): the record is current, the client
* refetches it.
* ({"type":"turn-settled"} + [DONE]): the client refetches the record.
* A retained settlement-failure notice precedes that terminal when a
* reply could not be saved before the submitter attached.
*
* 404 mirrors the POST handler's unknown-chat. Framework route is a thin
* wrapper over the server barrel (the import law); tests drive the seams
@@ -18,6 +19,6 @@ export async function GET(_req: Request, ctx: { params: Promise<{ id: string }>
const { id } = await ctx.params;
if (!getChat(id)) return Response.json({ error: "Unknown chat" }, { status: 404 });
const handle = runningTurn(id);
if (!handle) return settledTurnResponse();
if (!handle) return settledTurnResponse(id);
return piTurnResponse(handle);
}
+6
View File
@@ -0,0 +1,6 @@
import { claudeCodeInstallationResponse } from "@/src/server";
/** Installation/version only; never reads auth status or makes a model request. */
export async function GET(request: Request) {
return claudeCodeInstallationResponse(request);
}
+9
View File
@@ -0,0 +1,9 @@
/** PR #62's single custom endpoint: discover/manual connect, offline pins,
* and disconnect. Host boundaries and mutations live behind the server barrel. */
import { customProviderGet, customProviderMutation } from "@/src/server";
export const dynamic = "force-dynamic";
export const GET = customProviderGet;
export const POST = customProviderMutation;
export const PATCH = customProviderMutation;
export const DELETE = customProviderMutation;
+2 -1
View File
@@ -10,7 +10,8 @@
* tests/auth-provider-keys.test.ts) — the settings UI reads it optionally
* with a graceful fallback.
*
* Masked only — last 4 + length; PLAINTEXT NEVER LEAVES THE STORE THROUGH
* Masked only — last 4 + length for ordinary keys; custom keys are fully
* redacted at every length (no credential suffix). PLAINTEXT NEVER LEAVES THE STORE THROUGH
* THIS API (owner decision). `source` tells the page whether the live
* value comes from the environment (dev .env.local / launch env) or the
* file, and `shadowedByEnv` explains why a saved key is not the live one
+9 -7
View File
@@ -5,12 +5,13 @@
* discipline, same as the per-chat and defaults routes); sections are
* independent, so partial writes can never clobber each other.
*
* section ∈ general | chat | agent | appearance | graph | compaction
* section ∈ general | chat | agent | appearance | graph | compaction | models | dictation
* Body: the complete desired section object. Parse + validate strictly
* (server/settings-body.ts): a bad shape/type is a 400 carrying the
* supported list where one exists. The chat and models sections
* additionally pass the catalog gate (validateChatSettings /
* validateModelsSettings — unknown provider/model = 400). Thinking
* validateModelsSettings — unknown provider/model = 400, except unchanged
* model-slot selections may remain saved while unavailable). Thinking
* levels never reject a write (owner, interlude 2026-09-22): a pinned
* level CLAMPS to the named model's nearest supported level and the
* clamped value is stored — model swaps just work, same as the
@@ -63,6 +64,8 @@ export async function PUT(request: Request, ctx: { params: Promise<{ section: st
if (!parsed.ok) {
return Response.json({ error: parsed.error }, { status: 400 });
}
// Compare against the same record that this whole-section write replaces.
const record = readSettingsRecord() ?? { version: 1 };
let agentHealed: AgentSettings | undefined;
// Section-specific cross-validation (against the catalog / the prompt
@@ -73,11 +76,11 @@ export async function PUT(request: Request, ctx: { params: Promise<{ section: st
return Response.json({ error: verdict.error, supported: verdict.supported }, { status: 400 });
}
}
// Model slots wave (2026-09-20): the models section gets the same
// catalog gate the chat section gets — a slot must name a real
// provider+model and a thinking level it supports.
// New/replaced slot models pass the catalog gate. Unchanged saved model
// references may survive disconnect, including thinking-preference edits,
// so each unavailable slot can be repaired independently of the others.
if (section === "models") {
const verdict = validateModelsSettings(parsed.settings as ModelsSettings);
const verdict = validateModelsSettings(parsed.settings as ModelsSettings, record.models);
if (!verdict.ok) {
return Response.json({ error: verdict.error, supported: verdict.supported }, { status: 400 });
}
@@ -124,7 +127,6 @@ export async function PUT(request: Request, ctx: { params: Promise<{ section: st
// Read-modify-write ONE section of the record; everything else rides
// along untouched (other sections, unknown keys — sanitize preserves).
const record = readSettingsRecord() ?? { version: 1 };
const next = { ...record, [section]: sectionOut };
writeSettingsRecord(next);
return Response.json({ settings: next });
+6
View File
@@ -0,0 +1,6 @@
import { subscriptionBrowserResponse } from "@/src/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const GET = subscriptionBrowserResponse;
export const POST = subscriptionBrowserResponse;
+6
View File
@@ -0,0 +1,6 @@
import { subscriptionControlsResponse } from "@/src/server";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const GET = subscriptionControlsResponse;
export const POST = subscriptionControlsResponse;
+2 -1
View File
@@ -1,7 +1,7 @@
import { notFound } from "next/navigation";
import { Chat } from "@/components/chat";
import { slotsConfigured } from "@/src/core";
import { compactionInFlight, getChat, isTurnRunning, readSettingsRecord } from "@/src/server";
import { compactionInFlight, getChat, isTurnRunning, readSettingsRecord, turnFailure } from "@/src/server";
export const dynamic = "force-dynamic";
@@ -29,6 +29,7 @@ export default async function ChatPage({
// derived app-side (core/view.ts projectView; GET serves the same
// native messages).
initialMessages={chat.messages}
initialError={turnFailure(chat.id)}
initialConfigured={slotsConfigured(record?.models)}
initialPrompt={prompt}
// A teacher turn that outlived its client: the remounted view must