Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c396212
test(vision): pin model-aware reasoning contracts
Wibias Aug 7, 2026
0296bde
feat(vision): add sidecar reasoning ladder
Wibias Aug 7, 2026
ae07613
feat(vision): normalize reasoning by model capability
Wibias Aug 7, 2026
dfb0129
feat(models): expose native reasoning ladders
Wibias Aug 7, 2026
27dd486
feat(vision): send configured reasoning effort
Wibias Aug 7, 2026
c48be17
feat(vision): apply reasoning at runtime without losing raw-body sync
Wibias Aug 7, 2026
d8c5fea
refactor(management): preserve current config route base for takeover
Wibias Aug 7, 2026
2c7c6c9
feat(management): normalize vision reasoning at write boundary
Wibias Aug 7, 2026
910a621
feat(gui): add model-aware vision reasoning helpers
Wibias Aug 7, 2026
9707448
feat(gui): add vision reasoning selector
Wibias Aug 7, 2026
5a5e7f3
fix(gui): use shared clamp and existing localized reasoning label
Wibias Aug 7, 2026
ef15ee8
test(gui): cover vision reasoning ladder clamp
Wibias Aug 7, 2026
b5f334a
docs(vision): document model-aware reasoning
Wibias Aug 7, 2026
e758a0f
docs(vision): add Japanese reasoning guide
Wibias Aug 7, 2026
f3afa56
docs(vision): add Korean reasoning guide
Wibias Aug 7, 2026
7b9641a
docs(vision): add Russian reasoning guide
Wibias Aug 7, 2026
0296e98
docs(vision): add Chinese reasoning guide
Wibias Aug 7, 2026
5d81bbf
chore(vision): preserve existing describe comments
Wibias Aug 7, 2026
57763e8
fix(vision): avoid catalog facade on reasoning path
Wibias Aug 7, 2026
da70891
fix(vision): enforce reasoning in canonical sidecar route
Wibias Aug 7, 2026
96a6937
refactor(vision): remove duplicated config route base
Wibias Aug 7, 2026
185df75
chore: add one-shot maintainer fix runner
Wibias Aug 7, 2026
bb617c9
fix(vision): filter native sidecar effort metadata
Wibias Aug 7, 2026
1b55584
chore: make one-shot vision fix runner robust
Wibias Aug 7, 2026
a17b0c6
test(vision): cover management reasoning edge cases
Wibias Aug 7, 2026
31294f0
docs(vision): normalize OpenCodex product casing
Wibias Aug 7, 2026
b309855
fix(gui): align vision effort clamp with server
Wibias Aug 7, 2026
9d3efaa
test(gui): cover gapped vision effort ladders
Wibias Aug 7, 2026
fd800af
fix(gui): label vision reasoning control correctly
Wibias Aug 7, 2026
b7f605d
chore(vision): remove maintainer self-mutating workflow
Wibias Aug 7, 2026
1335296
fix(config): reject invalid vision reasoning in CLI writes
Wibias Aug 7, 2026
d0fb604
fix(config): normalize native vision effort pairs before CLI save
Wibias Aug 7, 2026
a572b04
fix(gui): hide stale unsupported vision efforts
Wibias Aug 7, 2026
b191e0f
fix(gui): persist effective vision model and effort
Wibias Aug 7, 2026
5136acd
test(gui): hide stale unsupported vision effort options
Wibias Aug 7, 2026
56a3fdb
docs(vision): update canonical sidecar reasoning references
Wibias Aug 7, 2026
b048406
test(vision): cover unset model reasoning fallback
Wibias Aug 7, 2026
e8e705f
fix(vision): align management fallback with runtime
Wibias Aug 7, 2026
8ab06f1
fix(management): restore tray validation message
Wibias Aug 7, 2026
2bc646a
fix(vision): normalize CLI reasoning against runtime default
Wibias Aug 7, 2026
0f29bf1
test(vision): cover CLI default-model normalization
Wibias Aug 7, 2026
b93b030
fix(gui): keep vision effort edits reasoning-only
Wibias Aug 7, 2026
f4736af
test(gui): preserve sidecar identity on effort edits
Wibias Aug 7, 2026
736f68e
fix(gui): preserve vision backend on effort changes
Wibias Aug 7, 2026
f583765
chore(gui): restore final newline
Wibias Aug 7, 2026
44745e8
chore(gui): restore overview final newline
Wibias Aug 7, 2026
cb6de74
fix(vision): report effective reasoning for default model
Wibias Aug 7, 2026
bcb6743
fix(vision): treat blank sidecar models as unset
Wibias Aug 7, 2026
9c82061
fix(vision): normalize blank CLI model as default
Wibias Aug 7, 2026
bdc6a9d
test(vision): cover blank and stale default-model configs
Wibias Aug 7, 2026
4fb06e0
docs(sidecars): clarify vision defaults and capability clamps
Wibias Aug 7, 2026
caaedb4
docs(ja): align vision defaults and clamp semantics
Wibias Aug 7, 2026
4fbd649
docs(ko): align vision defaults and clamp semantics
Wibias Aug 7, 2026
a1cd5fa
docs(ru): align vision defaults and clamp semantics
Wibias Aug 7, 2026
4ca1fd1
docs(zh-cn): align vision defaults and clamp semantics
Wibias Aug 7, 2026
6b1811a
feat(i18n): localize vision reasoning levels
Wibias Aug 7, 2026
621e616
fix(gui): localize vision reasoning labels
Wibias Aug 7, 2026
2a34906
test(gui): cover vision metadata and locale fallbacks
Wibias Aug 7, 2026
6300d0d
test(vision): use valid config fixture for CLI import
Wibias Aug 8, 2026
71978d0
test(vision): align Anthropic management DTO expectations
Wibias Aug 8, 2026
3d38a34
merge(dev): sync vision reasoning takeover with latest dev
Wibias Aug 8, 2026
dcad8bd
fix(codex): handle zero-byte first-use coordinator race
Wibias Aug 8, 2026
3069468
test(codex): pin first-use zero-byte coordinator race
Wibias Aug 8, 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
22 changes: 22 additions & 0 deletions docs-site/src/content/docs/guides/vision-sidecar-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "Vision Sidecar Reasoning"
description: Configure OpenAI vision-sidecar reasoning effort safely by model capability.
---

The OpenAI vision sidecar can use a configurable reasoning effort when it describes images for a text-only routed model.

Set `visionSidecar.reasoning` to `low`, `medium`, `high`, `xhigh`, or `max`. The default remains `low`.

```json
{
"visionSidecar": {
"backend": "openai",
"model": "gpt-5.6-luna",
"reasoning": "medium"
}
}
```

Supported levels depend on the selected model. The Dashboard reads each native model's advertised reasoning ladder and clamps an unavailable saved value to the highest supported rung. The management API and runtime apply the same model-aware normalization, so a direct API call or stale config cannot send a known-unsupported native effort upstream. Unknown/custom models remain permissive when opencodex has no reliable capability metadata.

Changing the OpenAI reasoning effort creates a distinct image-description cache identity. Anthropic vision ignores this OpenAI-specific setting, so its cache identity does not change.
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/ja/guides/vision-sidecar-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: "Vision サイドカーの推論負荷"
description: OpenAI Vision サイドカーの推論負荷をモデル能力に合わせて安全に設定します。
---

OpenAI の Vision サイドカーは、テキスト専用のルーティングモデル向けに画像を説明するときの推論負荷を設定できます。

`visionSidecar.reasoning` には `low`、`medium`、`high`、`xhigh`、`max` を指定できます。既定値は `low` のままです。

対応レベルは選択したモデルに依存します。ダッシュボードはネイティブモデルが公開する推論ラダーを使用し、未対応の保存値をそのモデルの最高対応ラングへ切り詰めます。管理 API と実行時にも同じ正規化を行うため、直接の API 呼び出しや古い設定から既知の未対応値が上流へ送られることはありません。信頼できる能力メタデータがないカスタムモデルは制限しません。

OpenAI では推論負荷が画像説明キャッシュの識別子に含まれます。Anthropic はこの OpenAI 固有設定を無視するため、Anthropic のキャッシュ識別子には含まれません。
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/ko/guides/vision-sidecar-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: "Vision 사이드카 추론 강도"
description: OpenAI Vision 사이드카의 추론 강도를 모델 기능에 맞게 안전하게 설정합니다.
---

OpenAI Vision 사이드카는 텍스트 전용 라우팅 모델을 위해 이미지를 설명할 때 사용할 추론 강도를 설정할 수 있습니다.

`visionSidecar.reasoning`은 `low`, `medium`, `high`, `xhigh`, `max`를 지원하며 기본값은 계속 `low`입니다.

지원 수준은 선택한 모델에 따라 달라집니다. 대시보드는 네이티브 모델이 공개한 추론 사다리를 읽고 지원되지 않는 저장값을 해당 모델의 최고 지원 단계로 제한합니다. 관리 API와 런타임도 같은 모델 인식 정규화를 적용하므로 직접 API 호출이나 오래된 설정이 알려진 비지원 값을 업스트림으로 보내지 않습니다. 신뢰할 수 있는 기능 메타데이터가 없는 사용자 지정 모델은 제한하지 않습니다.

OpenAI에서는 추론 강도가 이미지 설명 캐시 식별자에 포함됩니다. Anthropic은 이 OpenAI 전용 설정을 무시하므로 Anthropic 캐시 식별자에는 포함되지 않습니다.
12 changes: 12 additions & 0 deletions docs-site/src/content/docs/ru/guides/vision-sidecar-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: "Уровень рассуждений Vision-сайдкара"
description: Безопасная настройка уровня рассуждений OpenAI Vision-сайдкара с учётом возможностей модели.
---

Для OpenAI Vision-сайдкара можно настроить уровень рассуждений, используемый при описании изображений для текстовой routed-модели.

`visionSidecar.reasoning` принимает `low`, `medium`, `high`, `xhigh` или `max`. Значение по умолчанию остаётся `low`.

Поддерживаемые уровни зависят от выбранной модели. Дашборд читает заявленную лестницу рассуждений нативной модели и ограничивает недоступное сохранённое значение наивысшим поддерживаемым уровнем. Management API и runtime применяют ту же нормализацию, поэтому прямой API-вызов или устаревший конфиг не отправит известный неподдерживаемый уровень upstream. Пользовательские модели без надёжных метаданных возможностей остаются без искусственных ограничений.

Для OpenAI уровень рассуждений входит в идентификатор кэша описания изображения. Anthropic игнорирует эту специфичную для OpenAI настройку, поэтому его кэш от неё не зависит.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: "Vision 侧车推理强度"
description: 按模型能力安全配置 OpenAI Vision 侧车的推理强度。
---

OpenAI Vision 侧车在为纯文本路由模型描述图像时,可以配置所使用的推理强度。

`visionSidecar.reasoning` 支持 `low`、`medium`、`high`、`xhigh` 和 `max`,默认值仍为 `low`。

支持的等级取决于所选模型。控制台会读取原生模型公布的推理阶梯,并把不受支持的已保存值限制到该模型支持的最高档位。管理 API 和运行时也使用同一套按模型能力归一化逻辑,因此直接 API 调用或旧配置不会把已知不受支持的原生档位发送到上游。对于没有可靠能力元数据的自定义模型,opencodex 保持宽松处理。
Comment thread
Wibias marked this conversation as resolved.
Outdated

在 OpenAI 路径中,推理强度会进入图像描述缓存标识。Anthropic 会忽略这个 OpenAI 专用设置,因此 Anthropic 的缓存标识不会因它变化。
69 changes: 25 additions & 44 deletions gui/src/pages/dashboard-overview-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Trans } from "../i18n/provider";
import { Select } from "../ui";
import { formatNamespacedModelId } from "../provider-icons";
import { navigateHash } from "../hash-routing";
import { EFFORT_CAP_LEVELS, requireJson, shadowCallModelOptions, sidecarBackendForModel, updateJobLabel } from "./dashboard-shared";
import { clampVisionReasoningToLadder, EFFORT_CAP_LEVELS, requireJson, shadowCallModelOptions, sidecarBackendForModel, updateJobLabel, visionReasoningLadder, visionReasoningOptionsFor } from "./dashboard-shared";
import { shadowSourceModelBadge } from "./shadow-call-source";
import type { useDashboardData } from "./use-dashboard-data";

Expand Down Expand Up @@ -91,14 +91,6 @@ export function DashboardEffortCapPanel({ apiBase, d }: { apiBase: string; d: Da
);
}

/**
* Delegation row: pick the model (and effort) inline, with a link to the rest.
*
* The two switches moved to the Subagents tab, which is where the roster they affect lives.
* The model pick stays: it is the same shape as the sidecar rows below it (label left,
* dropdown right), and it is the one delegation choice worth changing without leaving the
* status page.
*/
export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) {
const {
t, injectionModel, injectionEffort, injectionEfforts, injectionAvailable, injectionSaving,
Expand Down Expand Up @@ -131,11 +123,7 @@ export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) {
label={t("dash.injectionEffortLabel")}
/>
)}
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => navigateHash("#subagents")}
>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => navigateHash("#subagents")}>
{t("dash.injectionManage")}
</button>
</div>
Expand All @@ -148,23 +136,11 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
t, runSync, syncing, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen,
syncResult, syncError, updateJob, reconnecting, clearSyncFeedback,
} = d;

// A sync result that carries actionable guidance (generic warning, native subagent
// defaults override, or the stale app-server hint) is the ONLY place that warning is
// visible, so it must not vanish on a timer: it stays until the next sync or an
// explicit dismiss.
const syncHoldsWarning = !!syncResult && (
!!syncResult.warning
|| !!syncResult.nativeSubagentDefaultsWarning
|| !!syncResult.staleAppServerHint
);

// Sync feedback is a transient fixed toast instead of an inline notice: the toast sits
// outside the layout flow, so the result can appear without pushing the panels below
// this card down by a full box height (the old notice shifted the whole dashboard on
// every sync click). Plain results auto-dismiss; a new sync clears and re-arms it.
// Dismissal is published to the dashboard data (clearSyncFeedback), not just a local
// flag, so switching tabs and back cannot resurrect a stale result as a fresh toast.
const [syncToastDismissed, setSyncToastDismissed] = useState(false);
const syncToastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Expand All @@ -186,14 +162,11 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
};
}, [syncResult, syncError, syncHoldsWarning, clearSyncFeedback]);

// A fresh click re-arms the toast even if the previous one was already auto-dismissed.
const handleRunSync = () => {
setSyncToastDismissed(false);
void runSync();
};

// Shared dismiss affordance for the sync toast: closes it locally AND clears the
// dashboard-level result so it cannot remount as fresh on the next Overview visit.
const dismissSyncToast = () => {
setSyncToastDismissed(true);
clearSyncFeedback();
Expand All @@ -202,7 +175,6 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
return (
<>
<div className="panel maintenance-panel">
{/* Same one-row chrome as Sub-agent delegation: copy left, action right. */}
<div className="dash-sync-summary">
<div className="dash-sync-copy">
<div className="font-semibold">{t("dash.syncModels")}</div>
Expand All @@ -212,13 +184,6 @@ export function DashboardMaintenancePanel({ d }: { d: Dash }) {
<button type="button" className="btn btn-ghost btn-sm" onClick={handleRunSync} disabled={syncing}>
<IconRefresh className={syncing ? "spin-icon" : undefined} /> {syncing ? t("dash.syncing") : t("dash.syncRun")}
</button>
{/*
The update flow lives in the sidebar footer, which reports whether one is waiting
and is reachable from every page. A second button here duplicated it without
adding that signal. The trigger stays as a zero-size anchor so the deep link
(`#dashboard/update`) still has something to open against and the dialog has a
focus target to return to on close.
*/}
<button
ref={updateTriggerRef}
type="button"
Expand Down Expand Up @@ -277,6 +242,9 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {
sidecar, sidecarSaving, sidecarModels, models, saveSidecar,
shadowCall, shadowCallSaving, shadowCallHelpTriggerRef, shadowCallHelpOpen, setShadowCallHelpOpen, saveShadowCall,
} = d;
const visionModel = sidecar?.vision.model ?? "gpt-5.6-luna";
const visionReasoning = sidecar?.vision.reasoning ?? "low";
const visionLadder = visionReasoningLadder(models, visionModel);

return (
<>
Expand Down Expand Up @@ -317,13 +285,26 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) {
<div className="panel dash-sidecar-card" aria-busy={!sidecar || undefined}>
<div className="dash-sidecar-card__row">
<div className="font-semibold">{t("dash.visionSidecar")}</div>
<Select
value={sidecar?.vision.model ?? "gpt-5.6-luna"}
options={sidecarModels}
onChange={model => { void saveSidecar({ vision: { model, backend: sidecarBackendForModel(models, model) } }); }}
disabled={!sidecar || sidecarSaving}
label={t("dash.sidecarModel")}
/>
<div className="dash-delegation-controls">
<Select
value={visionModel}
options={sidecarModels}
onChange={model => {
const ladder = visionReasoningLadder(models, model);
const reasoning = clampVisionReasoningToLadder(ladder, visionReasoning);
void saveSidecar({ vision: { model, backend: sidecarBackendForModel(models, model), reasoning } });
}}
disabled={!sidecar || sidecarSaving}
label={t("dash.sidecarModel")}
/>
<Select
value={visionReasoning}
options={visionReasoningOptionsFor(visionLadder, visionReasoning).map(value => ({ value, label: value }))}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
onChange={reasoning => { void saveSidecar({ vision: { reasoning: reasoning as typeof visionReasoning } }); }}
disabled={!sidecar || sidecarSaving}
label={t("dash.searchReasoning")}
/>
Comment thread
Wibias marked this conversation as resolved.
</div>
</div>
<div className="muted setting-hint">{t("dash.visionSidecarHint")}</div>
</div>
Expand Down
45 changes: 28 additions & 17 deletions gui/src/pages/dashboard-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@ import type { TKey } from "../i18n/shared";
import type { StartupHealthStatus } from "../startup-health-ui";

export type DashboardSection = "overview" | "providers" | "models";

/**
* `#dashboard/update` is the sidebar's action deep link. It is not a tab, so it resolves
* to Overview (where the maintenance panel lives) and separately asks the dashboard to
* open the update dialog.
*/
export const DASHBOARD_UPDATE_HASH = "dashboard/update";

export function readDashboardSectionFromHash(): DashboardSection {
Expand All @@ -20,17 +14,14 @@ export function readDashboardSectionFromHash(): DashboardSection {
return "overview";
}

/** True while the location hash is the sidebar update deep link. */
export function hashRequestsUpdateDialog(): boolean {
return window.location.hash.replace(/^#\/?/, "") === DASHBOARD_UPDATE_HASH;
}

/** Overview is the bare `#dashboard`; the other sections carry a suffix. */
export function dashboardHashForSection(section: DashboardSection): string {
return section === "overview" ? "dashboard" : `dashboard/${section}`;
}

/** Like readJsonOrThrow, but rejects empty/204 bodies that would otherwise yield undefined. */
export async function requireJson<T>(res: Response, fallbackMessage?: string): Promise<T> {
const data = await readJsonOrThrow<T>(res, fallbackMessage);
if (data === undefined) throw new Error(fallbackMessage ?? "empty response");
Expand All @@ -39,12 +30,11 @@ export async function requireJson<T>(res: Response, fallbackMessage?: string): P

export interface HealthData { status: string; version: string; uptime: number }
export interface ProviderInfo { name: string; adapter: string; baseUrl: string; defaultModel?: string; hasApiKey: boolean }
export interface ModelInfo { id: string; provider: string; namespaced: string; owned_by?: string }
export interface ModelInfo { id: string; provider: string; namespaced: string; owned_by?: string; reasoningEfforts?: string[] }
export interface SettingsData {
codexAutoStart: boolean;
port: number;
hostname: string;
/** IANA zone of the machine running the proxy, used to render log timestamps (#725). */
timeZone?: string;
startupHealth?: {
status: "native" | "protected" | "at-risk";
Expand All @@ -55,11 +45,12 @@ export interface SettingsData {
};
}
export type SidecarBackend = "openai" | "anthropic";
export interface SidecarSetting { backend?: SidecarBackend; model: string }
export type VisionReasoning = "low" | "medium" | "high" | "xhigh" | "max";
export interface SidecarSetting { backend?: SidecarBackend; model: string; reasoning?: VisionReasoning }
export interface SidecarData { webSearch: SidecarSetting; vision: SidecarSetting }
export interface SidecarPatch {
webSearch?: { backend?: SidecarBackend | null; model?: string };
vision?: { backend?: SidecarBackend | null; model?: string };
vision?: { backend?: SidecarBackend | null; model?: string; reasoning?: VisionReasoning };
}
export interface ShadowCallData { enabled: boolean; model: string; sourceModels?: string[] }
export interface UsageSummary30d { summary: { requests: number; totalTokens: number; coverageRatio: number } }
Expand Down Expand Up @@ -142,15 +133,38 @@ export function updateJobLabel(status: UpdateJobStatus, t: (key: TKey) => string

export function mergeSidecarSetting(
current: SidecarSetting,
update?: { backend?: SidecarBackend | null; model?: string },
update?: { backend?: SidecarBackend | null; model?: string; reasoning?: VisionReasoning },
): SidecarSetting {
const merged = { ...current };
if (update?.model !== undefined) merged.model = update.model;
if (update?.backend === null) delete merged.backend;
else if (update?.backend !== undefined) merged.backend = update.backend;
if (update?.reasoning !== undefined) merged.reasoning = update.reasoning;
return merged;
}

export const VISION_REASONING_LEVELS: VisionReasoning[] = ["low", "medium", "high", "xhigh", "max"];

export function visionReasoningLadder(models: ModelInfo[], modelId: string): VisionReasoning[] {
const model = models.find(m => m.id === modelId);
const ladder = model?.reasoningEfforts;
if (!ladder || ladder.length === 0) return [...VISION_REASONING_LEVELS];
const supported = VISION_REASONING_LEVELS.filter(effort => ladder.includes(effort));
return supported.length > 0 ? supported : [...VISION_REASONING_LEVELS];
}

export function visionReasoningOptionsFor(ladder: VisionReasoning[], persisted: VisionReasoning): VisionReasoning[] {
return ladder.includes(persisted) ? ladder : [persisted, ...ladder];
Comment thread
Wibias marked this conversation as resolved.
Outdated
}

/** Preserve a supported value; otherwise clamp to the highest rung the selected model advertises. */
export function clampVisionReasoningToLadder(
ladder: VisionReasoning[],
persisted: VisionReasoning,
): VisionReasoning {
return ladder.includes(persisted) ? persisted : ladder[ladder.length - 1];
}
Comment thread
Wibias marked this conversation as resolved.
Outdated

export function sidecarModelOptions(models: ModelInfo[]) {
const out: Array<{ value: string; label: string }> = [];
for (const model of models) {
Expand All @@ -161,7 +175,6 @@ export function sidecarModelOptions(models: ModelInfo[]) {
return out;
}

/** Options for shadow-call replacement models use the proxy's canonical routing id. */
export function shadowCallModelOptions(models: ModelInfo[], current: string | undefined) {
const out = [{ value: "", label: "—" }, ...models.map(model => ({ value: model.namespaced, label: model.namespaced }))];
if (current && !out.some(option => option.value === current)) out.push({ value: current, label: current });
Expand Down Expand Up @@ -197,12 +210,10 @@ export function useModalDialog(open: boolean, triggerRef: RefObject<HTMLButtonEl
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;

if (open) {
if (!dialog.open) dialog.showModal();
return;
}

if (dialog.open) dialog.close();
focusTriggerQuietly(triggerRef.current);
}, [open, triggerRef]);
Expand Down
Loading
Loading