Merge upstream v0.4.0 into Gitea fork
This commit is contained in:
@@ -57,6 +57,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { MenuShell } from "@/components/menu-shell";
|
||||
import { useSettingsSaved } from "@/components/settings-signal";
|
||||
import { defaultOrderLevel, populateLevel } from "@/components/thinking-default";
|
||||
import { useModelCatalog } from "@/components/model-catalog";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
import type React from "react";
|
||||
import {
|
||||
COMPACTION_THRESHOLD_FLOOR,
|
||||
@@ -65,7 +67,6 @@ import {
|
||||
type AgentSettings,
|
||||
type CompactionSettings,
|
||||
type GraphSettings,
|
||||
type ModelCatalogEntry,
|
||||
type ModelSlotName,
|
||||
type ModelsSettings,
|
||||
} from "@/src/core";
|
||||
@@ -73,8 +74,7 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
||||
import { prettyModel } from "@/components/usage-format";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ZapIcon } from "lucide-react";
|
||||
import { Hint } from "@/components/ui/hint";
|
||||
import { SettingsGroup, SettingsRow, Toggle, CommitInput } from "./controls.tsx";
|
||||
import { SettingsGroup, SettingsRow, SettingsMenuTrigger, Toggle, CommitInput } from "./controls.tsx";
|
||||
import { ModelMenuList, ThinkingMenuList } from "./model-menu";
|
||||
import { ChatGPTBackgroundAccess } from "./chatgpt-connection";
|
||||
import { NativeSubscriptionBackgroundAccess } from "./subscription-connections";
|
||||
@@ -105,15 +105,6 @@ interface SegmentInfo {
|
||||
shared: boolean;
|
||||
}
|
||||
|
||||
/** The GET /api/models response fields this section consumes. */
|
||||
interface ModelsBody {
|
||||
models: ModelCatalogEntry[];
|
||||
/** The three tiers' RESOLVED pins (BYOK: NULL per unset tier — no
|
||||
* factory floor) — machinery only (the thinking list fallback); the
|
||||
* rows' displays read the record override, never a phantom pin. */
|
||||
slots: Record<ModelSlotName, { model: { provider: string; id: string }; thinkingLevel?: string } | null>;
|
||||
}
|
||||
|
||||
/** Compose: one tool toggled, the rest of the section carried verbatim.
|
||||
* Pure — exported for tests. */
|
||||
export function composeTool(agent: AgentSettings | undefined, id: string, on: boolean): AgentSettings {
|
||||
@@ -247,8 +238,10 @@ function MenuTrigger({
|
||||
onClick,
|
||||
widthClass,
|
||||
disabled,
|
||||
hint,
|
||||
}: {
|
||||
value: string;
|
||||
hint?: string;
|
||||
/** What the trigger shows when there is no override ("Default"). */
|
||||
emptyLabel: string;
|
||||
ariaLabel: string;
|
||||
@@ -264,9 +257,9 @@ function MenuTrigger({
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
const button = (
|
||||
<SettingsMenuTrigger
|
||||
value={t(value || emptyLabel)}
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={open}
|
||||
disabled={disabled}
|
||||
@@ -274,16 +267,11 @@ function MenuTrigger({
|
||||
onClick={() => {
|
||||
if (!disabled) onClick();
|
||||
}}
|
||||
className={`flex h-7 max-w-full items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors ${
|
||||
disabled ? "cursor-not-allowed" : "hover:border-muted-foreground/50"
|
||||
} ${widthClass}`}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{t(value || emptyLabel)}</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
</button>
|
||||
unavailable={disabled}
|
||||
className={widthClass}
|
||||
/>
|
||||
);
|
||||
return hint ? <Hint label={hint}>{button}</Hint> : button;
|
||||
}
|
||||
|
||||
/* The section's dropdowns all mount the shared MenuShell (fix wave chunk
|
||||
@@ -319,19 +307,19 @@ export function AgentSection({
|
||||
onCompactionChange: (next: CompactionSettings) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [catalog, setCatalog] = useState<ModelCatalogEntry[] | null>(null);
|
||||
const [savingModels, setSavingModels] = useState(false);
|
||||
const [modelsError, setModelsError] = useState<string | null>(null);
|
||||
const updateModels = async (next: ModelsSettings) => {
|
||||
if (savingModels) return;
|
||||
setSavingModels(true); setModelsError(null);
|
||||
setSavingModels(true);
|
||||
setModelsError(null);
|
||||
try { await onModelsChange(next); }
|
||||
catch { setModelsError("Could not save model settings"); }
|
||||
finally { setSavingModels(false); }
|
||||
};
|
||||
/** The three tiers' resolved pins — GET /api/models `slots`. The rows'
|
||||
* Default hints and thinking lists resolve from it. */
|
||||
const [slots, setSlots] = useState<ModelsBody["slots"] | null>(null);
|
||||
const { catalog: body, failed: catalogFailed, refresh: refreshCatalog } = useModelCatalog();
|
||||
const catalog = body?.models;
|
||||
const slots = body?.slots;
|
||||
/** The live search state (/api/search) — the row's reason line. */
|
||||
const [search, setSearch] = useState<{ available: boolean; engine: string | null; reason: string } | null>(null);
|
||||
/** Which search engines have a key (/api/keys) — the no-keys-anywhere
|
||||
@@ -366,28 +354,9 @@ export function AgentSection({
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// The catalog (federated: keyed providers only), the resolved slots,
|
||||
// the prompt segments, and the search engines' key presence — one
|
||||
// fetch each per section mount. The models read ALSO re-runs on the
|
||||
// cross-surface signal (fix wave: another surface's write re-reads
|
||||
// into the slot rows live).
|
||||
const modelsRequest = useRef(0);
|
||||
const modelsMounted = useRef(false);
|
||||
const readModels = useCallback((connectionChanged = false) => {
|
||||
const request = ++modelsRequest.current;
|
||||
if (connectionChanged) setCatalog(null);
|
||||
fetch("/api/models")
|
||||
.then((r) => { if (!r.ok) throw new Error("Catalog unavailable"); return r.json(); })
|
||||
.then((body: ModelsBody) => {
|
||||
if (!modelsMounted.current || request !== modelsRequest.current) return;
|
||||
setCatalog(body.models);
|
||||
setSlots(body.slots);
|
||||
})
|
||||
.catch(() => { if (modelsMounted.current && request === modelsRequest.current) setCatalog(null); });
|
||||
}, []);
|
||||
|
||||
// The shared catalog seeds the first frame and refreshes per mount and
|
||||
// keys/models signal. Settings props still own saved selections/levels.
|
||||
useSettingsSaved((section) => {
|
||||
if (section === "models" || section === "keys") readModels(section === "keys");
|
||||
// THE state-echo root cause: the prompt switches write through the
|
||||
// agent section, but this view (the toggles' truth) never re-read —
|
||||
// the settings-saved law (every reader re-reads) was missing this
|
||||
@@ -396,8 +365,6 @@ export function AgentSection({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
modelsMounted.current = true;
|
||||
readModels();
|
||||
fetch("/api/settings/prompts")
|
||||
.then((r) => r.json())
|
||||
.then((body: { prober: SegmentInfo[]; teacher: SegmentInfo[] }) => setPrompts(body))
|
||||
@@ -413,7 +380,6 @@ export function AgentSection({
|
||||
setEngineKeys(present);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => { modelsMounted.current = false; modelsRequest.current++; };
|
||||
}, []);
|
||||
|
||||
// The reason line re-reads /api/search AFTER an engine write lands —
|
||||
@@ -657,9 +623,6 @@ export function AgentSection({
|
||||
const chosen = coord ? catalog?.find((m) => m.id === coord.id && m.provider === coord.provider) : undefined;
|
||||
const levels = chosen ? chosen.supportedLevels : THINKING_LEVELS;
|
||||
const thisMenuOpen = openMenu?.target === slot ? openMenu.kind : null;
|
||||
// Vision slots pick from image-capable models only.
|
||||
const slotCatalog =
|
||||
slot === "vision" ? catalog?.filter((m) => m.input.includes("image")) : catalog;
|
||||
return (
|
||||
<SettingsRow
|
||||
key={slot}
|
||||
@@ -720,9 +683,10 @@ export function AgentSection({
|
||||
menuClass="flex w-[22rem] flex-col rounded-xl border border-border bg-card p-2 text-xs shadow-xl"
|
||||
>
|
||||
<ModelMenuList
|
||||
models={slotCatalog ?? undefined}
|
||||
models={catalog ?? undefined}
|
||||
effectiveId={modelId}
|
||||
effectiveProvider={provider}
|
||||
vision={slot === "vision"}
|
||||
onPick={(entry) => {
|
||||
// Auto-populate (chunk 2, owner: "populate everywhere"): the
|
||||
// fresh pick writes a level — the stored one rides as-is
|
||||
|
||||
Reference in New Issue
Block a user