Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- *(webui-v2)* report settings imports with no supported entries as failures instead of showing a false success message ([#6179](https://github.com/nearai/ironclaw/issues/6179)).
- *(filesystem)* make libSQL descendant listings seek through the path index instead of scanning the full root-filesystem table, preventing extension-readiness fan-out from stalling unrelated WebUI requests.
- *(webui-v2)* expose per-user secret provisioning in Admin user details with write-only values, handle-only listings, and confirmed deletion ([#6118](https://github.com/nearai/ironclaw/issues/6118)).
- *(webui-v2)* render the Extensions Registry as soon as catalog data arrives instead of holding the skeleton screen for slower installed-extension enrichment ([#6052](https://github.com/nearai/ironclaw/issues/6052)).
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("ar", {
"settings.exportSuccess": "تم تصدير الإعدادات",
"settings.importSuccess": "تم استيراد الإعدادات",
"settings.importInvalid": "يجب أن يحتوي الملف المحدد على كائن إعدادات",
"settings.importNoSupported": "لا يحتوي الملف المحدد على إعدادات مدعومة",
"settings.importFailed": "فشل الاستيراد: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("de", {
"settings.exportSuccess": "Einstellungen exportiert",
"settings.importSuccess": "Einstellungen importiert",
"settings.importInvalid": "Ausgewählte Datei muss ein Einstellungsobjekt enthalten",
"settings.importNoSupported": "Die ausgewählte Datei enthält keine unterstützten Einstellungen",
"settings.importFailed": "Import fehlgeschlagen: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ registerPack("en", {
"settings.exportSuccess": "Settings exported",
"settings.importSuccess": "Settings imported",
"settings.importInvalid": "Selected file must contain a settings object",
"settings.importNoSupported": "No supported settings found in the selected file",
"settings.importFailed": "Import failed: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("es", {
"settings.exportSuccess": "Configuración exportada",
"settings.importSuccess": "Configuración importada",
"settings.importInvalid": "El archivo seleccionado debe contener un objeto de configuración",
"settings.importNoSupported": "El archivo seleccionado no contiene ajustes compatibles",
"settings.importFailed": "Importación fallida: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("fr", {
"settings.exportSuccess": "Paramètres exportés",
"settings.importSuccess": "Paramètres importés",
"settings.importInvalid": "Le fichier sélectionné doit contenir un objet de paramètres",
"settings.importNoSupported": "Aucun paramètre pris en charge dans le fichier sélectionné",
"settings.importFailed": "Échec de l'importation : {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("hi", {
"settings.exportSuccess": "निर्यात की गई सेटिंग्स",
"settings.importSuccess": "आयातित सेटिंग्स",
"settings.importInvalid": "चयनित फ़ाइल में एक सेटिंग ऑब्जेक्ट होना चाहिए",
"settings.importNoSupported": "चयनित फ़ाइल में कोई समर्थित सेटिंग नहीं मिली",
"settings.importFailed": "आयात विफल: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("ja", {
"settings.exportSuccess": "エクスポートされた設定",
"settings.importSuccess": "インポートされた設定",
"settings.importInvalid": "選択したファイルには設定オブジェクトが含まれている必要があります",
"settings.importNoSupported": "選択したファイルにサポートされている設定がありません",
"settings.importFailed": "インポートに失敗しました: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("ko", {
"settings.exportSuccess": "내보낸 설정",
"settings.importSuccess": "가져온 설정",
"settings.importInvalid": "선택한 파일에 설정 개체가 포함되어야 함",
"settings.importNoSupported": "선택한 파일에 지원되는 설정이 없습니다",
"settings.importFailed": "가져오기 실패: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("pt-BR", {
"settings.exportSuccess": "Configurações exportadas",
"settings.importSuccess": "Configurações importadas",
"settings.importInvalid": "O arquivo selecionado deve conter um objeto de configurações",
"settings.importNoSupported": "Nenhuma configuração compatível foi encontrada no arquivo selecionado",
"settings.importFailed": "Falha na importação: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/uk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("uk", {
"settings.exportSuccess": "Налаштування експортовано",
"settings.importSuccess": "Налаштування імпортовано",
"settings.importInvalid": "Вибраний файл має містити об’єкт налаштувань",
"settings.importNoSupported": "У вибраному файлі немає підтримуваних налаштувань",
"settings.importFailed": "Помилка імпорту: {message}",

// Settings — restart banner
Expand Down
1 change: 1 addition & 0 deletions crates/ironclaw_webui/frontend/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ registerPack("zh-CN", {
"settings.exportSuccess": "设置已导出",
"settings.importSuccess": "设置已导入",
"settings.importInvalid": "所选文件必须包含设置对象",
"settings.importNoSupported": "所选文件中没有受支持的设置",
"settings.importFailed": "导入失败:{message}",

// Settings — restart banner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Icon } from "../../../design-system/icons";
import React from "react";
import { useT } from "../../../lib/i18n";
import { saveBlob } from "../../../lib/download";
import { NoSupportedSettingsImportError } from "../lib/settings-api";

function downloadJson(filename, data) {
saveBlob(
Expand Down Expand Up @@ -86,6 +87,10 @@ export function SettingsToolbar({
await onImport(payload);
showMessage("success", t("settings.importSuccess"));
} catch (error) {
if (error instanceof NoSupportedSettingsImportError) {
showMessage("error", t("settings.importNoSupported"));
return;
}
showMessage(
"error",
t("settings.importFailed", { message: error.message })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
fetchSettingsExport,
importSettings as importSettingsPayload,
NoSupportedSettingsImportError,
updateSetting,
} from "../lib/settings-api";
import { throwIfApiFailed } from "../lib/api-result";
Expand Down Expand Up @@ -58,7 +59,13 @@ export function useSettings() {
);

const importMutation = useMutation({
mutationFn: importSettingsPayload,
mutationFn: async (payload) => {
const result = await importSettingsPayload(payload);
if (result.success === false) {
throw new NoSupportedSettingsImportError(result);
}
return result;
},
onSuccess: (_data, payload) => {
queryClient.invalidateQueries({ queryKey: ["settings-export"] });
const importedKeys = Object.keys(payload?.settings || {});
Expand All @@ -85,6 +92,5 @@ export function useSettings() {
importSettings,
isImporting: importMutation.isPending,
saveError: mutation.error,
importError: importMutation.error,
};
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import { test } from "vitest";

import { settingsFromOperatorConfig, toolFromConfigEntry } from "./settings-api";
import {
settingsFromOperatorConfig,
toolFromConfigEntry,
} from "./settings-api";

test("settingsFromOperatorConfig maps the global auto-approve key", () => {
const settings = settingsFromOperatorConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,49 @@ export async function updateSetting(key, value) {
});
return { success: true, entry: data.entry, value: data.entry?.value };
}
export async function importSettings(payload) {

type SettingsImportUpdateResult = Awaited<ReturnType<typeof updateSetting>>;

export type SettingsImportSuccess = {
success: true;
imported: number;
results: SettingsImportUpdateResult[];
};

export type NoSupportedSettingsImportFailure = {
success: false;
imported: 0;
results: SettingsImportUpdateResult[];
message: string;
};

export type SettingsImportResult =
| SettingsImportSuccess
| NoSupportedSettingsImportFailure;

export class NoSupportedSettingsImportError extends Error {
constructor(failure: NoSupportedSettingsImportFailure) {
super(failure.message);
this.name = "NoSupportedSettingsImportError";
}
}

export async function importSettings(
payload: { settings?: Record<string, unknown> } | null | undefined
): Promise<SettingsImportResult> {
const settings = payload?.settings || {};
const imported = [];
const imported: SettingsImportUpdateResult[] = [];
if (Object.prototype.hasOwnProperty.call(settings, AUTO_APPROVE_KEY)) {
imported.push(await updateSetting(AUTO_APPROVE_KEY, Boolean(settings[AUTO_APPROVE_KEY])));
}
if (imported.length === 0) {
return {
success: false,
imported: 0,
results: imported,
message: "No supported settings were found in the selected file",
};
}
return { success: true, imported: imported.length, results: imported };
}
// LLM provider configuration — v2 native endpoints. The snapshot is the single
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export function SettingsPage() {
importSettings,
isImporting,
saveError,
importError,
} = useSettings();
const [searchQuery, setSearchQuery] = React.useState("");

Expand Down Expand Up @@ -122,15 +121,6 @@ export function SettingsPage() {
</div>
)}

{importError &&
(
<div
className="rounded-xl border border-red-400/30 bg-red-500/10 px-4 py-3 text-sm text-red-200"
>
{t("settings.importFailed", { message: importError.message })}
</div>
)}

<SettingsToolbar
settingsExport={query.data || null}
onImport={importSettings}
Expand Down
2 changes: 2 additions & 0 deletions tests/e2e/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
"header_docs_link": "[data-testid='header-docs-link']",
"command_palette_dialog_name": "Command palette",
"command_palette_search_placeholder": "Type a command or search",
"command_palette_go_settings_name": "Go to Settings",
"auth_gate": "[data-testid='auth-gate']",
"auth_gate_for": "[data-testid='auth-gate'][data-auth-challenge='{kind}']",
"auth_token_input": "[data-testid='auth-token-input']",
Expand Down Expand Up @@ -359,6 +360,7 @@
"logs_entry_context": "[data-testid='logs-entry-context']",
"logs_context_chip": "[data-testid='logs-context-chip'][data-context-key='{key}']",
"settings_search_placeholder": "Search settings...",
"settings_import_file": 'input[type="file"][accept=".json,application/json"]',
"settings_tool_row_for": (
"[data-testid='settings-tool-row'][data-tool-name='{name}']"
),
Expand Down
62 changes: 62 additions & 0 deletions tests/e2e/scenarios/test_reborn_webui_v2_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,68 @@ async def test_reborn_v2_appearance_theme_selection_persists(reborn_v2_page):
)


async def test_reborn_v2_settings_import_rejects_unsupported_payloads(
reborn_v2_page,
):
"""Unsupported imports show one localized error and do not refresh settings."""
settings_reads = 0

def count_settings_reads(request) -> None:
nonlocal settings_reads
if (
request.method == "GET"
and urlparse(request.url).path == "/api/webchat/v2/settings/tools"
):
settings_reads += 1

reborn_v2_page.on("request", count_settings_reads)
await reborn_v2_page.keyboard.press("Control+K")
command_palette = reborn_v2_page.get_by_role(
"dialog", name=SEL_V2["command_palette_dialog_name"]
)
await expect(command_palette).to_be_visible()
await command_palette.get_by_role(
"button", name=SEL_V2["command_palette_go_settings_name"]
).click()
await reborn_v2_page.wait_for_url(
re.compile(r".*/settings(?:[?#].*)?$")
)
file_input = reborn_v2_page.locator(SEL_V2["settings_import_file"])
await expect(file_input).to_have_count(1, timeout=15000)
await reborn_v2_page.wait_for_timeout(250)
initial_settings_reads = settings_reads

for filename, settings in [
("empty-settings.json", {}),
("unsupported-settings.json", {"agent.model": "example-model"}),
]:
await file_input.set_input_files(
{
"name": filename,
"mimeType": "application/json",
"buffer": json.dumps({"settings": settings}).encode(),
}
)
status = reborn_v2_page.get_by_role("status").filter(
has_text="No supported settings found in the selected file"
)
await expect(status).to_have_count(1)
await expect(status).to_have_text(
"No supported settings found in the selected file"
)
await expect(
reborn_v2_page.get_by_text("Settings imported", exact=True)
).to_have_count(0)
await expect(
reborn_v2_page.get_by_text(re.compile(r"^Import failed:"))
).to_have_count(0)

await reborn_v2_page.wait_for_timeout(250)
assert settings_reads == initial_settings_reads, (
"failed settings imports unexpectedly invalidated the settings query"
)


async def test_reborn_v2_text_turn_persists(reborn_v2_server):
"""A text turn over /api/webchat/v2/* completes and persists one assistant reply."""
headers = {"Authorization": f"Bearer {REBORN_V2_AUTH_TOKEN}"}
Expand Down
Loading