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
18 changes: 18 additions & 0 deletions src-tauri/crates/settings/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ mod tests {

assert!(result.is_ok());
}

#[test]
fn app_license_reads_the_repository_license() {
let license = app_license_read();

assert!(license.starts_with("GNU AFFERO GENERAL PUBLIC LICENSE"));
assert!(license.contains("Version 3, 19 November 2007"));
}
}

/// Reset settings by deleting the file.
Expand All @@ -108,6 +116,16 @@ pub async fn settings_get_path() -> Result<String, String> {
Ok(file_io::get_settings_path().to_string_lossy().to_string())
}

/// Return the license shipped with the application.
///
/// Embedding the repository's root LICENSE at compile time keeps the viewer
/// synchronized with the license that is shipped in each build without
/// duplicating its contents in frontend code.
#[tauri::command]
pub fn app_license_read() -> String {
include_str!("../../../../LICENSE").to_string()
}

/// Write the JSON Schema file alongside the settings file.
/// The schema enables autocomplete in external editors (VS Code, etc.).
#[tauri::command]
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/handler_list.inc
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ settings::commands::settings_write_partial,
settings::commands::settings_reset,
settings::commands::settings_get_path,
settings::commands::settings_write_schema,
settings::commands::app_license_read,
// Codex config (~/.codex/config.toml)
agent_cli::codex::commands::codex_config_read,
agent_cli::codex::commands::codex_config_write_partial,
Expand Down
18 changes: 18 additions & 0 deletions src/api/tauri/license.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it, vi } from "vitest";

import { readAppLicense } from "./license";

const invoke = vi.hoisted(() => vi.fn());

vi.mock("@tauri-apps/api/core", () => ({ invoke }));

describe("readAppLicense", () => {
it("requests the license from the desktop runtime", async () => {
invoke.mockResolvedValue("GNU AFFERO GENERAL PUBLIC LICENSE");

await expect(readAppLicense()).resolves.toContain(
"GNU AFFERO GENERAL PUBLIC LICENSE"
);
expect(invoke).toHaveBeenCalledWith("app_license_read");
});
});
6 changes: 6 additions & 0 deletions src/api/tauri/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { invoke } from "@tauri-apps/api/core";

/** Read the license text embedded in the running desktop application. */
export function readAppLicense(): Promise<string> {
return invoke<string>("app_license_read");
}
8 changes: 7 additions & 1 deletion src/i18n/locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Akzent",
"linkSkinVariants": "Hell und Dunkel koppeln",
"linkSkinVariantsDesc": "Verwendet einen Skin und Akzent für beide Modi. Nur Skins mit heller und dunkler Variante können gekoppelt werden"
"linkSkinVariantsDesc": "Verwendet einen Skin und Akzent für beide Modi. Nur Skins mit heller und dunkler Variante können gekoppelt werden",
"license": "Lizenz",
"licenseDesc": "Die mit dieser App gebündelte Lizenz anzeigen",
"viewLicense": "Lizenz anzeigen",
"licenseTitle": "ORG2-Lizenz",
"licenseLoading": "Lizenz wird geladen…",
"licenseLoadFailed": "Die App-Lizenz konnte nicht geladen werden"
},
"background": {
"title": "Hintergrund",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,13 @@
"rainbow": "Rainbow",
"dark": "Dark tile",
"light": "Light tile"
}
},
"license": "License",
"licenseDesc": "View the license bundled with this app",
"viewLicense": "View license",
"licenseTitle": "ORG2 license",
"licenseLoading": "Loading license…",
"licenseLoadFailed": "Unable to load the application license"
},
"background": {
"title": "Background",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Acento",
"linkSkinVariants": "Vincular claro y oscuro",
"linkSkinVariantsDesc": "Usa una sola skin y un solo acento para ambos modos. Solo se pueden vincular las skins que incluyen una variante clara y una oscura"
"linkSkinVariantsDesc": "Usa una sola skin y un solo acento para ambos modos. Solo se pueden vincular las skins que incluyen una variante clara y una oscura",
"license": "Licencia",
"licenseDesc": "Ver la licencia incluida con esta aplicación",
"viewLicense": "Ver licencia",
"licenseTitle": "Licencia de ORG2",
"licenseLoading": "Cargando licencia…",
"licenseLoadFailed": "No se puede cargar la licencia de la aplicación"
},
"background": {
"title": "Fondo",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Accent",
"linkSkinVariants": "Lier clair et sombre",
"linkSkinVariantsDesc": "Utiliser un seul skin et un seul accent pour les deux modes. Seuls les skins proposant une variante claire et une variante sombre peuvent être liés"
"linkSkinVariantsDesc": "Utiliser un seul skin et un seul accent pour les deux modes. Seuls les skins proposant une variante claire et une variante sombre peuvent être liés",
"license": "Licence",
"licenseDesc": "Afficher la licence incluse avec cette application",
"viewLicense": "Afficher la licence",
"licenseTitle": "Licence ORG2",
"licenseLoading": "Chargement de la licence…",
"licenseLoadFailed": "Impossible de charger la licence de l’application"
},
"background": {
"title": "Arrière-plan",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "スキン",
"accent": "アクセント",
"linkSkinVariants": "ライトとダークを連動",
"linkSkinVariantsDesc": "両方のモードで 1 つのスキンとアクセントを使用します。ライトとダークの両バリアントを備えたスキンのみ連動できます"
"linkSkinVariantsDesc": "両方のモードで 1 つのスキンとアクセントを使用します。ライトとダークの両バリアントを備えたスキンのみ連動できます",
"license": "ライセンス",
"licenseDesc": "このアプリに同梱されているライセンスを表示します",
"viewLicense": "ライセンスを表示",
"licenseTitle": "ORG2 のライセンス",
"licenseLoading": "ライセンスを読み込み中…",
"licenseLoadFailed": "アプリのライセンスを読み込めません"
},
"background": {
"title": "背景",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ko/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "스킨",
"accent": "강조색",
"linkSkinVariants": "라이트와 다크 연동",
"linkSkinVariantsDesc": "두 모드에 하나의 스킨과 강조색을 사용합니다. 라이트와 다크 변형을 모두 제공하는 스킨만 연동할 수 있습니다"
"linkSkinVariantsDesc": "두 모드에 하나의 스킨과 강조색을 사용합니다. 라이트와 다크 변형을 모두 제공하는 스킨만 연동할 수 있습니다",
"license": "라이선스",
"licenseDesc": "이 앱에 포함된 라이선스 보기",
"viewLicense": "라이선스 보기",
"licenseTitle": "ORG2 라이선스",
"licenseLoading": "라이선스 로드 중…",
"licenseLoadFailed": "앱 라이선스를 불러올 수 없습니다"
},
"background": {
"title": "배경",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/pl/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skórka",
"accent": "Akcent",
"linkSkinVariants": "Powiąż jasny i ciemny",
"linkSkinVariantsDesc": "Używaj jednej skórki i akcentu w obu trybach. Powiązać można tylko skórki, które mają wariant jasny i ciemny"
"linkSkinVariantsDesc": "Używaj jednej skórki i akcentu w obu trybach. Powiązać można tylko skórki, które mają wariant jasny i ciemny",
"license": "Licencja",
"licenseDesc": "Wyświetl licencję dołączoną do tej aplikacji",
"viewLicense": "Wyświetl licencję",
"licenseTitle": "Licencja ORG2",
"licenseLoading": "Wczytywanie licencji…",
"licenseLoadFailed": "Nie można wczytać licencji aplikacji"
},
"background": {
"title": "Tło",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/pt/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Destaque",
"linkSkinVariants": "Vincular claro e escuro",
"linkSkinVariantsDesc": "Usa um único skin e destaque para os dois modos. Só é possível vincular skins que incluem uma variante clara e uma escura"
"linkSkinVariantsDesc": "Usa um único skin e destaque para os dois modos. Só é possível vincular skins que incluem uma variante clara e uma escura",
"license": "Licença",
"licenseDesc": "Ver a licença incluída com este aplicativo",
"viewLicense": "Ver licença",
"licenseTitle": "Licença do ORG2",
"licenseLoading": "Carregando licença…",
"licenseLoadFailed": "Não foi possível carregar a licença do aplicativo"
},
"background": {
"title": "Plano de fundo",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Скин",
"accent": "Акцент",
"linkSkinVariants": "Связать светлый и тёмный",
"linkSkinVariantsDesc": "Использовать один скин и акцент для обоих режимов. Связать можно только скины, имеющие светлый и тёмный варианты"
"linkSkinVariantsDesc": "Использовать один скин и акцент для обоих режимов. Связать можно только скины, имеющие светлый и тёмный варианты",
"license": "Лицензия",
"licenseDesc": "Просмотреть лицензию, включённую в это приложение",
"viewLicense": "Просмотреть лицензию",
"licenseTitle": "Лицензия ORG2",
"licenseLoading": "Загрузка лицензии…",
"licenseLoadFailed": "Не удалось загрузить лицензию приложения"
},
"background": {
"title": "Фон",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/tr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Vurgu rengi",
"linkSkinVariants": "Açık ve koyuyu bağla",
"linkSkinVariantsDesc": "Her iki mod için tek bir skin ve vurgu rengi kullan. Yalnızca hem açık hem koyu varyantı olan skin'ler bağlanabilir"
"linkSkinVariantsDesc": "Her iki mod için tek bir skin ve vurgu rengi kullan. Yalnızca hem açık hem koyu varyantı olan skin'ler bağlanabilir",
"license": "Lisans",
"licenseDesc": "Bu uygulamayla birlikte gelen lisansı görüntüle",
"viewLicense": "Lisansı görüntüle",
"licenseTitle": "ORG2 lisansı",
"licenseLoading": "Lisans yükleniyor…",
"licenseLoadFailed": "Uygulama lisansı yüklenemedi"
},
"background": {
"title": "Arka Plan",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/vi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "Skin",
"accent": "Màu nhấn",
"linkSkinVariants": "Liên kết sáng và tối",
"linkSkinVariantsDesc": "Dùng một skin và màu nhấn cho cả hai chế độ. Chỉ các skin có cả biến thể sáng và tối mới có thể liên kết"
"linkSkinVariantsDesc": "Dùng một skin và màu nhấn cho cả hai chế độ. Chỉ các skin có cả biến thể sáng và tối mới có thể liên kết",
"license": "Giấy phép",
"licenseDesc": "Xem giấy phép đi kèm ứng dụng này",
"viewLicense": "Xem giấy phép",
"licenseTitle": "Giấy phép ORG2",
"licenseLoading": "Đang tải giấy phép…",
"licenseLoadFailed": "Không thể tải giấy phép của ứng dụng"
},
"background": {
"title": "Hình nền",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/zh-Hant/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,13 @@
"skin": "外觀主題",
"accent": "強調色",
"linkSkinVariants": "連動淺色與深色",
"linkSkinVariantsDesc": "兩種模式使用同一套外觀主題和強調色。只有同時提供淺色與深色變體的外觀主題才能連動"
"linkSkinVariantsDesc": "兩種模式使用同一套外觀主題和強調色。只有同時提供淺色與深色變體的外觀主題才能連動",
"license": "授權條款",
"licenseDesc": "查看此應用程式隨附的授權條款",
"viewLicense": "查看授權條款",
"licenseTitle": "ORG2 授權條款",
"licenseLoading": "正在載入授權條款…",
"licenseLoadFailed": "無法載入應用程式授權條款"
},
"background": {
"title": "背景",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/zh/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,13 @@
"rainbow": "彩虹",
"dark": "深色",
"light": "浅色"
}
},
"license": "许可证",
"licenseDesc": "查看此应用附带的许可证",
"viewLicense": "查看许可证",
"licenseTitle": "ORG2 许可证",
"licenseLoading": "正在加载许可证…",
"licenseLoadFailed": "无法加载应用许可证"
},
"background": {
"title": "背景",
Expand Down
17 changes: 17 additions & 0 deletions src/modules/MainApp/Settings/sections/GeneralSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
HighRefreshRateRow,
} from "./HighRefreshRateRow";
import HttpVersionSettingsBlock from "./HttpVersionSettingsBlock";
import LicenseModal from "./LicenseModal";

export const GENERAL_TAB_KEYS = {
GENERAL: "general",
Expand Down Expand Up @@ -145,6 +146,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({

const GeneralTabBody: React.FC = () => {
const { t, i18n } = useTranslation("settings");
const [licenseModalVisible, setLicenseModalVisible] = useState(false);
const [timezone, setTimezone] = useAtom(timezoneAtom);
const [chatAppearance, updateChatAppearance] = useAtom(
chatAppearancePersistAtom
Expand Down Expand Up @@ -516,6 +518,21 @@ const GeneralTabBody: React.FC = () => {
/>
</SectionContainer>
)}

<SectionContainer>
<SectionRow
label={t("general.license")}
description={t("general.licenseDesc")}
>
<Button size="default" onClick={() => setLicenseModalVisible(true)}>
{t("general.viewLicense")}
</Button>
</SectionRow>
</SectionContainer>

{licenseModalVisible && (
<LicenseModal visible onClose={() => setLicenseModalVisible(false)} />
)}
</>
);
};
Expand Down
68 changes: 68 additions & 0 deletions src/modules/MainApp/Settings/sections/LicenseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";

import { readAppLicense } from "@src/api/tauri/license";
import Button from "@src/components/Button";
import Modal from "@src/scaffold/ModalSystem";

interface LicenseModalProps {
visible: boolean;
onClose: () => void;
}

const LicenseModal: React.FC<LicenseModalProps> = ({ visible, onClose }) => {
const { t } = useTranslation("settings");
const [licenseText, setLicenseText] = useState<string | null>(null);
const [loadError, setLoadError] = useState(false);

useEffect(() => {
if (!visible) return;

let cancelled = false;

readAppLicense()
.then((text) => {
if (!cancelled) setLicenseText(text);
})
.catch(() => {
if (!cancelled) setLoadError(true);
});

return () => {
cancelled = true;
};
}, [visible]);

return (
<Modal
visible={visible}
title={t("general.licenseTitle")}
onClose={onClose}
size="large"
bodyClassName="min-h-0 p-0"
footer={
<div className="flex justify-end px-3 py-2">
<Button variant="primary" size="small" onClick={onClose}>
{t("common:actions.close")}
</Button>
</div>
}
>
<div className="allow-select-deep min-h-0 overflow-y-auto px-4 py-3">
{loadError ? (
<p className="text-sm text-danger-6">
{t("general.licenseLoadFailed")}
</p>
) : licenseText === null ? (
<p className="text-sm text-text-2">{t("general.licenseLoading")}</p>
) : (
<pre className="m-0 font-mono text-xs leading-5 break-words whitespace-pre-wrap text-text-1">
{licenseText}
</pre>
)}
</div>
</Modal>
);
};

export default LicenseModal;
Loading