diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index c631ffd4..d0110db3 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -10915,47 +10915,6 @@ } } }, - "Automatic learning reviews completed requests after 10 turns or 15 tool calls. Corrections are reviewed sooner. Your configured model may incur API charges." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisches Lernen prüft abgeschlossene Anfragen nach 10 Gesprächsrunden oder 15 Werkzeugaufrufen. Korrekturen werden früher geprüft. Für Ihr konfiguriertes Modell können API-Gebühren anfallen." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "L’apprentissage automatique examine les demandes terminées après 10 échanges ou 15 appels d’outils. Les corrections sont examinées plus tôt. Le modèle configuré peut entraîner des frais d’API." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動学習では、10ターンまたは15回のツール呼び出しの後に、完了したリクエストをレビューします。修正指示はそれより早くレビューされます。設定したモデルのAPI利用料金が発生する場合があります。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "자동 학습은 10턴 또는 도구 호출 15회 후에 완료된 요청을 검토합니다. 수정 요청은 더 일찍 검토합니다. 설정한 모델의 API 사용 요금이 발생할 수 있습니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "自动学习会在完成10轮请求或15次工具调用后审查对话;纠正会更早审查。使用所配置的模型可能产生API费用。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "自動學習會在完成10輪請求或15次工具呼叫後審查對話;更正會更早審查。使用所設定的模型可能產生API費用。" - } - } - } - }, "Automatically Rename Workspaces" : { "extractionState" : "manual", "localizations" : { @@ -11876,6 +11835,47 @@ } } }, + "Built-in Server" : { + "extractionState" : "manual", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Integrierter Server" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Serveur intégré" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "組み込みサーバ" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "내장 서버" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "内置服务器" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "內建伺服器" + } + } + } + }, "Built-in Themes" : { "extractionState" : "manual", "localizations" : { @@ -12987,47 +12987,6 @@ } } }, - "Changes apply to queued transfers; active transfers continue. If new connections would prompt for authentication, transfers use parallel channels on the existing connection." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Änderungen gelten für wartende Übertragungen; aktive Übertragungen laufen weiter. Wenn neue Verbindungen eine Authentifizierung erfordern würden, verwenden Übertragungen parallele Kanäle der bestehenden Verbindung." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les modifications s’appliquent aux transferts en attente ; les transferts actifs continuent. Si de nouvelles connexions devaient demander une authentification, les transferts utilisent des canaux parallèles sur la connexion existante." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "変更は待機中の転送に適用されます。進行中の転送はそのまま続行します。新しい接続で認証が必要になる場合は、既存の接続上で並列チャネルを使用します。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "변경 사항은 대기 중인 전송에 적용되며, 진행 중인 전송은 계속됩니다. 새 연결에 인증이 필요한 경우 기존 연결의 병렬 채널을 사용합니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改会应用到排队中的传输;正在进行的传输会继续。如果新连接会提示身份验证,传输将使用现有连接上的并行通道。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "更改會套用至佇列中的傳輸;正在進行的傳輸會繼續。如果新連線會提示驗證,傳輸將使用現有連線上的平行通道。" - } - } - } - }, "Changes file ownership or permissions." : { "extractionState" : "manual", "localizations" : { @@ -17158,49 +17117,6 @@ } } }, - "Connect to remote MCP servers over HTTP." : { - "comment" : "A description of MCP servers.", - "extractionState" : "manual", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über HTTP mit entfernten MCP-Servern verbinden." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connectez-vous à des serveurs MCP distants via HTTP." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "HTTP経由でリモートMCPサーバに接続します。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "HTTP를 통해 원격 MCP 서버에 연결합니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "通过HTTP连接远程MCP服务器。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "透過HTTP連線遠端MCP伺服器。" - } - } - } - }, "Connect to see what this server offers." : { "extractionState" : "manual", "localizations" : { @@ -28458,43 +28374,43 @@ } } }, - "Every model searches Google, Bing, DuckDuckGo, and Yahoo from this Mac and reads result pages as Markdown." : { + "Every model searches Google, Bing, DuckDuckGo, and Yahoo." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Jedes Modell durchsucht Google, Bing, DuckDuckGo und Yahoo von diesem Mac aus und liest Ergebnisseiten als Markdown." + "value" : "Jedes Modell durchsucht Google, Bing, DuckDuckGo und Yahoo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Chaque modèle effectue des recherches sur Google, Bing, DuckDuckGo et Yahoo depuis ce Mac et lit les pages de résultats en Markdown." + "value" : "Chaque modèle effectue des recherches sur Google, Bing, DuckDuckGo et Yahoo." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "すべてのモデルがこのMacからGoogle、Bing、DuckDuckGo、Yahooを検索し、結果のページをMarkdownとして読み取ります。" + "value" : "すべてのモデルがGoogle、Bing、DuckDuckGo、Yahooを検索します。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모든 모델이 이 Mac에서 Google, Bing, DuckDuckGo 및 Yahoo를 검색하고 결과 페이지를 Markdown으로 읽습니다." + "value" : "모든 모델이 Google, Bing, DuckDuckGo 및 Yahoo를 검색합니다." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "所有模型都从这台Mac搜索Google、Bing、DuckDuckGo和Yahoo,并以Markdown读取结果页面。" + "value" : "所有模型都搜索Google、Bing、DuckDuckGo和Yahoo。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "所有模型都從這台Mac搜尋Google、Bing、DuckDuckGo和Yahoo,並以Markdown讀取結果頁面。" + "value" : "所有模型都搜尋Google、Bing、DuckDuckGo和Yahoo。" } } } @@ -37825,43 +37741,43 @@ } } }, - "Limits open shell, execute, and SFTP channels on each SSH connection. Port forwarding does not use this limit. Changes apply to new channels immediately; active channels continue." : { + "Limits open shell, execute, and SFTP channels on each SSH connection. Port forwarding does not use this limit." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Begrenzt offene Shell-, Ausführungs- und SFTP-Kanäle pro SSH-Verbindung. Portweiterleitungen unterliegen dieser Begrenzung nicht. Änderungen gelten sofort für neue Kanäle; aktive Kanäle laufen weiter." + "value" : "Begrenzt offene Shell-, Ausführungs- und SFTP-Kanäle pro SSH-Verbindung. Portweiterleitungen unterliegen dieser Begrenzung nicht." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Limite les canaux shell, d’exécution et SFTP ouverts sur chaque connexion SSH. La redirection de port n’est pas concernée. Les modifications s’appliquent immédiatement aux nouveaux canaux ; les canaux actifs continuent." + "value" : "Limite les canaux shell, d’exécution et SFTP ouverts sur chaque connexion SSH. La redirection de port n’est pas concernée." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "各SSH接続で開くシェル、コマンド実行、SFTPのチャネル数を制限します。ポート転送はこの制限の対象外です。変更は新しいチャネルにすぐに適用され、使用中のチャネルはそのまま継続します。" + "value" : "各SSH接続で開くシェル、コマンド実行、SFTPのチャネル数を制限します。ポート転送はこの制限の対象外です。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "각 SSH 연결에서 열 수 있는 셸, 명령 실행 및 SFTP 채널 수를 제한합니다. 포트 포워딩에는 이 제한이 적용되지 않습니다. 변경 사항은 새 채널에 즉시 적용되며, 사용 중인 채널은 계속 유지됩니다." + "value" : "각 SSH 연결에서 열 수 있는 셸, 명령 실행 및 SFTP 채널 수를 제한합니다. 포트 포워딩에는 이 제한이 적용되지 않습니다." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "限制每条SSH连接上打开的Shell、执行和SFTP通道数。端口转发不受此限制。更改会立即应用于新通道,活动通道将继续运行。" + "value" : "限制每条SSH连接上打开的Shell、执行和SFTP通道数。端口转发不受此限制。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "限制每條SSH連線上開啟的Shell、執行和SFTP通道數。連接埠轉送不受此限制。變更會立即套用於新通道,作用中的通道將繼續執行。" + "value" : "限制每條SSH連線上開啟的Shell、執行和SFTP通道數。連接埠轉送不受此限制。" } } } @@ -40046,19 +39962,20 @@ } } }, - "Minimum Duration (seconds)" : { + "Minimum Duration (s)" : { + "comment" : "Minimum command duration for notifications; s is the seconds unit in this full label, not a standalone letter.", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Mindestdauer (Sekunden)" + "value" : "Mindestdauer (s)" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Durée minimale (secondes)" + "value" : "Durée minimale (s)" } }, "ja" : { @@ -40503,43 +40420,43 @@ } } }, - "Models with provider-native search use it directly. Other models search Google, Bing, DuckDuckGo, and Yahoo from this Mac and read result pages as Markdown." : { + "Models with provider-native search use it directly. Other models search Google, Bing, DuckDuckGo, and Yahoo." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Modelle mit anbietereigener Suche verwenden diese direkt. Andere Modelle durchsuchen Google, Bing, DuckDuckGo und Yahoo von diesem Mac aus und lesen Ergebnisseiten als Markdown." + "value" : "Modelle mit anbietereigener Suche verwenden diese direkt. Andere Modelle durchsuchen Google, Bing, DuckDuckGo und Yahoo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "Les modèles disposant d’une recherche native du fournisseur l’utilisent directement. Les autres recherchent sur Google, Bing, DuckDuckGo et Yahoo depuis ce Mac et lisent les pages de résultats en Markdown." + "value" : "Les modèles disposant d’une recherche native du fournisseur l’utilisent directement. Les autres recherchent sur Google, Bing, DuckDuckGo et Yahoo." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "プロバイダのネイティブ検索に対応したモデルは、その機能を直接使用します。それ以外のモデルは、このMacからGoogle、Bing、DuckDuckGo、Yahooを検索し、結果のページをMarkdownとして読み取ります。" + "value" : "プロバイダのネイティブ検索に対応したモデルは、その機能を直接使用します。それ以外のモデルは、Google、Bing、DuckDuckGo、Yahooを検索します。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "제공업체의 자체 검색을 지원하는 모델은 해당 기능을 직접 사용합니다. 다른 모델은 이 Mac에서 Google, Bing, DuckDuckGo 및 Yahoo를 검색하고 결과 페이지를 Markdown으로 읽습니다." + "value" : "제공업체의 자체 검색을 지원하는 모델은 해당 기능을 직접 사용합니다. 다른 모델은 Google, Bing, DuckDuckGo 및 Yahoo를 검색합니다." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "支持提供方原生搜索的模型会直接使用该能力;其他模型从这台Mac搜索Google、Bing、DuckDuckGo和Yahoo,并以Markdown读取结果页面。" + "value" : "支持提供方原生搜索的模型会直接使用该能力;其他模型搜索Google、Bing、DuckDuckGo和Yahoo。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "支援供應商原生搜尋的模型會直接使用該能力;其他模型從這台Mac搜尋Google、Bing、DuckDuckGo和Yahoo,並以Markdown讀取結果頁面。" + "value" : "支援供應商原生搜尋的模型會直接使用該能力;其他模型搜尋Google、Bing、DuckDuckGo和Yahoo。" } } } @@ -56790,47 +56707,6 @@ } } }, - "Requests go directly from this Mac to each endpoint." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anfragen werden direkt von diesem Mac an den jeweiligen Endpunkt gesendet." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les requêtes sont envoyées directement depuis ce Mac vers chaque point de terminaison." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "リクエストはこのMacから各エンドポイントに直接送信されます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "요청은 이 Mac에서 각 엔드포인트로 직접 전송됩니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "请求会从这台Mac直接发送到各端点。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "請求會從這台Mac直接傳送到各端點。" - } - } - } - }, "Requests use the %@ format." : { "extractionState" : "manual", "localizations" : { @@ -60726,47 +60602,6 @@ } } }, - "Saved memory stays on this device. The assistant searches it when earlier preferences or decisions are relevant." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gespeicherte Erinnerungen bleiben auf diesem Gerät. Der Assistent durchsucht sie, wenn frühere Präferenzen oder Entscheidungen relevant sind." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Les souvenirs enregistrés restent sur cet appareil. L’assistant les consulte lorsque des préférences ou des décisions antérieures sont pertinentes." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "メモリはこのデバイスに保存されます。過去の好みや判断が関係する場合、アシスタントが検索します。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "기억은 이 기기에 저장됩니다. 이전의 선호 사항이나 결정이 관련된 경우 어시스턴트가 기억을 검색합니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "记忆保存在此设备上。涉及以往偏好或决定时,助理会按需搜索。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "記憶保存在此裝置上。涉及以往偏好或決定時,助理會視需要搜尋。" - } - } - } - }, "Saved workspaces could not be restored. The saved data has been kept." : { "extractionState" : "manual", "localizations" : { @@ -68847,47 +68682,6 @@ } } }, - "Termind connects directly from this Mac over HTTP." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Termind verbindet sich direkt von diesem Mac über HTTP." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Termind se connecte directement depuis ce Mac via HTTP." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "TermindはこのMacからHTTP経由で直接接続します。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "Termind는 이 Mac에서 HTTP로 직접 연결합니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "Termind会通过HTTP从这台Mac直接连接。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "Termind會透過HTTP從這台Mac直接連線。" - } - } - } - }, "Termind could not start an audio input. Check that a microphone is connected and available." : { "extractionState" : "manual", "localizations" : { @@ -70314,49 +70108,6 @@ } } }, - "The assistant loads and follows a skill when a task matches its description. Commands, writes, and transfers retain their usual approval rules." : { - "comment" : "A description of what a skill is.", - "extractionState" : "manual", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Assistent lädt und befolgt einen Skill, wenn eine Aufgabe seiner Beschreibung entspricht. Für Befehle, Schreibvorgänge und Übertragungen gelten weiterhin die üblichen Genehmigungsregeln." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "L’assistant charge et suit une compétence lorsqu’une tâche correspond à sa description. Les commandes, les écritures et les transferts conservent leurs règles d’autorisation habituelles." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "タスクがスキルの説明に一致すると、アシスタントはスキルを読み込み、その指示に従います。コマンド、書き込み、転送には、通常の承認ルールが引き続き適用されます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "작업이 스킬 설명과 일치하면 어시스턴트가 스킬을 불러와 지침을 따릅니다. 명령, 쓰기 및 전송에는 기존 승인 규칙이 그대로 적용됩니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "任务与技能描述相符时,助手会加载并遵循该技能。命令、写入和传输仍遵循原有的批准规则。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "任務與技能描述相符時,助手會載入並遵循該技能。指令、寫入和傳輸仍遵循原有的核准規則。" - } - } - } - }, "The bundled MCP bridge is missing. Codex and Hermes setup commands are unavailable." : { "extractionState" : "manual", "localizations" : { @@ -72134,47 +71885,6 @@ } } }, - "The key is stored in this Mac's Data Protection Keychain." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Der Schlüssel wird im datengeschützten Schlüsselbund dieses Macs gespeichert." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "La clé est stockée dans le trousseau de protection des données de ce Mac." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "鍵はこのMacのデータ保護キーチェーンに保存されます。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "키는 이 Mac의 데이터 보호 키체인에 저장됩니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "密钥存储在这台Mac的数据保护钥匙串中。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "金鑰儲存在這台Mac的資料保護鑰匙圈中。" - } - } - } - }, "The key or passphrase is invalid. Retry or deselect this key." : { "extractionState" : "manual", "localizations" : { @@ -87689,47 +87399,6 @@ } } }, - "macOS opens Termind when you log in, which also starts the local MCP server if you have it enabled." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS öffnet Termind bei Ihrer Anmeldung. Dabei wird auch der lokale MCP-Server gestartet, sofern er aktiviert ist." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS ouvre Termind à votre connexion, ce qui démarre aussi le serveur MCP local s’il est activé." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ログイン時にmacOSがTermindを開きます。ローカルMCPサーバが有効な場合は、同時に起動します。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "로그인하면 macOS가 Termind를 엽니다. 로컬 MCP 서버가 활성화되어 있으면 함께 시작됩니다." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS会在你登录时打开Termind,如果你启用了本地MCP服务器,它也会随之启动。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS會在你登入時開啟Termind,如果你啟用了本機MCP伺服器,它也會隨之啟動。" - } - } - } - }, "memory" : { "extractionState" : "manual", "localizations" : { diff --git a/Frameworks/TermindPresentation/MemorySettingsSection.swift b/Frameworks/TermindPresentation/MemorySettingsSection.swift index b02c7582..76af3375 100644 --- a/Frameworks/TermindPresentation/MemorySettingsSection.swift +++ b/Frameworks/TermindPresentation/MemorySettingsSection.swift @@ -129,8 +129,6 @@ public struct MemorySettingsSection: View { Task { await memory.setEnabled(enabled) } }) ).disabled(memory.isBusy) - Text(AppStrings.Settings.hermesPolicyDescription) - .font(.caption).foregroundStyle(.secondary) if memory.isEnabled { Toggle("Automatic Learning", isOn: $memory.reviewEnabled) Picker("Learning Model", selection: $memory.reviewModel) { @@ -144,8 +142,6 @@ public struct MemorySettingsSection: View { } } } - Text(AppStrings.Settings.hermesLearningDescription) - .font(.caption).foregroundStyle(.secondary) memoryRows } if let error = exportError ?? memory.error { diff --git a/Frameworks/TermindPresentation/NotificationSettingsFields.swift b/Frameworks/TermindPresentation/NotificationSettingsFields.swift index 692bf699..3876c881 100644 --- a/Frameworks/TermindPresentation/NotificationSettingsFields.swift +++ b/Frameworks/TermindPresentation/NotificationSettingsFields.swift @@ -47,12 +47,12 @@ public struct NotificationSettingsFields: View { Stepper(value: binding(\.terminalMinimumSeconds), in: 0...Int.max, step: 1) { #if os(macOS) HStack { - Text(AppStrings.CommandNotifications.seconds) + Text(AppStrings.CommandNotifications.minimumDuration) Spacer() Text(model.preferences.terminalMinimumSeconds, format: .number).monospacedDigit() } #else - LabeledContent(AppStrings.CommandNotifications.seconds.swiftUIKey) { + LabeledContent(AppStrings.CommandNotifications.minimumDuration.swiftUIKey) { Text(model.preferences.terminalMinimumSeconds, format: .number).monospacedDigit() } #endif diff --git a/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift b/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift index caa5f8d2..c879499a 100644 --- a/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift +++ b/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift @@ -31,8 +31,6 @@ public struct SFTPSettingsFields: View { Stepper(value: binding(\.perHostLimit), in: model.preferences.availablePerHostRange) { label(AppStrings.SFTP.perHostTransfers, value: model.preferences.perHostLimit) } - Text(AppStrings.SFTP.concurrencyChangesExplanation) - .font(.caption).foregroundStyle(.secondary) if let error = model.storageError { Text(error).foregroundStyle(.red) } } diff --git a/Frameworks/TermindPresentation/SSHSettingsFields.swift b/Frameworks/TermindPresentation/SSHSettingsFields.swift index 79c9a28e..cf82324a 100644 --- a/Frameworks/TermindPresentation/SSHSettingsFields.swift +++ b/Frameworks/TermindPresentation/SSHSettingsFields.swift @@ -15,10 +15,8 @@ public struct SSHSettingsFields: View { Text(model.preferences.maxSessions, format: .number).monospacedDigit() } } - Text( - "Limits open shell, execute, and SFTP channels on each SSH connection. Port forwarding does not use this limit. Changes apply to new channels immediately; active channels continue." - ) - .font(.caption).foregroundStyle(.secondary) + Text(AppStrings.Settings.sshMaxSessionsExplanation) + .font(.caption).foregroundStyle(.secondary) if let error = model.storageError { Text(error).foregroundStyle(.red) } } diff --git a/Frameworks/TermindPresentation/Strings/CommandNotifications.swift b/Frameworks/TermindPresentation/Strings/CommandNotifications.swift index 64d69884..680549d8 100644 --- a/Frameworks/TermindPresentation/Strings/CommandNotifications.swift +++ b/Frameworks/TermindPresentation/Strings/CommandNotifications.swift @@ -5,7 +5,7 @@ extension AppStrings { public static var when: LocalizedStringResource { "Notify When" } public static var never: LocalizedStringResource { "Never" } public static var always: LocalizedStringResource { "Always" } - public static var seconds: LocalizedStringResource { "Minimum Duration (seconds)" } + public static var minimumDuration: LocalizedStringResource { "Minimum Duration (s)" } public static var openSettings: LocalizedStringResource { "Open Notification Settings" } public static var denied: LocalizedStringResource { "Notifications are disabled in system settings." } public static var unread: LocalizedStringResource { "Unread command completion" } diff --git a/Frameworks/TermindPresentation/Strings/SFTP.swift b/Frameworks/TermindPresentation/Strings/SFTP.swift index 2835bca6..038b089f 100644 --- a/Frameworks/TermindPresentation/Strings/SFTP.swift +++ b/Frameworks/TermindPresentation/Strings/SFTP.swift @@ -11,10 +11,6 @@ extension AppStrings { public static var smartStrategyExplanation: LocalizedStringResource { "Smart uses channels for files up to 64 MB and prefers additional connections for larger files. Transfers with an unknown size use channels." } - public static var concurrencyChangesExplanation: LocalizedStringResource { - "Changes apply to queued transfers; active transfers continue. If new connections would prompt for authentication, transfers use parallel channels on the existing connection." - } - public static var appStoreProgress: LocalizedStringResource { "App Store…" } diff --git a/Frameworks/TermindPresentation/Strings/Settings.swift b/Frameworks/TermindPresentation/Strings/Settings.swift index 4f7d704f..38f3d511 100644 --- a/Frameworks/TermindPresentation/Strings/Settings.swift +++ b/Frameworks/TermindPresentation/Strings/Settings.swift @@ -125,6 +125,10 @@ extension AppStrings { "Bearer Token" } + public static var builtInServer: LocalizedStringResource { + "Built-in Server" + } + public static var callHistory: LocalizedStringResource { "Call History" } @@ -169,10 +173,6 @@ extension AppStrings { "Connect Clients" } - public static var connectToRemoteMcpServersOverHttp: LocalizedStringResource { - "Connect to remote MCP servers over HTTP." - } - public static var connectToSeeWhatThisServerOffers: LocalizedStringResource { "Connect to see what this server offers." } @@ -341,14 +341,6 @@ extension AppStrings { "User Profile" } - public static var hermesPolicyDescription: LocalizedStringResource { - "Saved memory stays on this device. The assistant searches it when earlier preferences or decisions are relevant." - } - - public static var hermesLearningDescription: LocalizedStringResource { - "Automatic learning reviews completed requests after 10 turns or 15 tool calls. Corrections are reviewed sooner. Your configured model may incur API charges." - } - public static var destroy: LocalizedStringResource { "Destroy" } public static var destroyMemoryConfirmation: LocalizedStringResource { "Destroy Memory?" } @@ -648,10 +640,6 @@ extension AppStrings { "Select \(label) for batch actions" } - public static var server: LocalizedStringResource { - "Server" - } - public static var serverDeleted: LocalizedStringResource { "Server Deleted" } @@ -692,10 +680,6 @@ extension AppStrings { "Describe what the skill does and when to use it so the assistant knows when to load it." } - public static var skillExplanation: LocalizedStringResource { - "The assistant loads and follows a skill when a task matches its description. Commands, writes, and transfers retain their usual approval rules." - } - public static var skills: LocalizedStringResource { "Skills" } @@ -728,18 +712,10 @@ extension AppStrings { "Termind uses [Ghostty](https://ghostty.org) as its terminal engine. Custom configuration uses [Ghostty's configuration format](https://ghostty.org/docs/config/reference)." } - public static var termindConnectsDirectlyFromThisMacOverHttp: LocalizedStringResource { - "Termind connects directly from this Mac over HTTP." - } - public static var testingProgress: LocalizedStringResource { "Testing…" } - public static var theKeyIsStoredInThisMacSDataProtectionKeychain: LocalizedStringResource { - "The key is stored in this Mac's Data Protection Keychain." - } - public static var theNewLanguageWillBeUsedAfterTermindRelaunches: LocalizedStringResource { "The new language will be used after Termind relaunches." } @@ -817,7 +793,7 @@ extension AppStrings { } public static var webSearchExplanation: LocalizedStringResource { - "Every model searches Google, Bing, DuckDuckGo, and Yahoo from this Mac and reads result pages as Markdown." + "Every model searches Google, Bing, DuckDuckGo, and Yahoo." } public static var hermesSetupRequirement: LocalizedStringResource { @@ -828,18 +804,18 @@ extension AppStrings { "Keeps the remote working directory current, records confirmed commands, and provides detected distribution, kernel, and architecture details to AI features." } + public static var sshMaxSessionsExplanation: LocalizedStringResource { + "Limits open shell, execute, and SFTP channels on each SSH connection. Port forwarding does not use this limit." + } + public static var nativeAndFallbackSearchExplanation: LocalizedStringResource { - "Models with provider-native search use it directly. Other models search Google, Bing, DuckDuckGo, and Yahoo from this Mac and read result pages as Markdown." + "Models with provider-native search use it directly. Other models search Google, Bing, DuckDuckGo, and Yahoo." } public static var modelPickerVisibilityExplanation: LocalizedStringResource { "Offer this model in the Assistant model picker and /model command." } - public static var directProviderConnectionExplanation: LocalizedStringResource { - "Requests go directly from this Mac to each endpoint." - } - public static var rightClickPasteExplanation: LocalizedStringResource { "Right-click to paste. Hold Option to keep the usual right-click behavior. Applications that capture the mouse keep their mouse controls." } @@ -884,10 +860,6 @@ extension AppStrings { "Update client configurations after changing the port or token, or after moving Termind for Codex and Hermes." } - public static var loginItemExplanation: LocalizedStringResource { - "macOS opens Termind when you log in, which also starts the local MCP server if you have it enabled." - } - public static var enterAValidProviderUrlOrAVertexProjectAndLocation: LocalizedStringResource { "Enter a valid provider URL, or a Vertex project and location." } diff --git a/TFI/Features/Settings/MobileAppSettingsView.swift b/TFI/Features/Settings/MobileAppSettingsView.swift index d6315a4b..a92bc888 100644 --- a/TFI/Features/Settings/MobileAppSettingsView.swift +++ b/TFI/Features/Settings/MobileAppSettingsView.swift @@ -52,7 +52,11 @@ struct MobileAppSettingsView: View { Section(AppStrings.Settings.support.swiftUIKey) { AppReviewLink(controller: appReview) { Label(AppStrings.Settings.rateOnTheAppStore.swiftUIKey, systemImage: "star") + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } + .buttonStyle(.plain) + .foregroundStyle(.tint) Link(destination: URL(string: "https://termind.akinokaede.com/support/")!) { Label(AppStrings.Settings.support.swiftUIKey, systemImage: "questionmark.circle") } diff --git a/TFI/Features/Settings/MobileMCPSettings.swift b/TFI/Features/Settings/MobileMCPSettings.swift index 154d3247..4a7456be 100644 --- a/TFI/Features/Settings/MobileMCPSettings.swift +++ b/TFI/Features/Settings/MobileMCPSettings.swift @@ -43,10 +43,10 @@ struct MobileMCPSettings: View { } } } - } footer: { - Text(AppStrings.Settings.connectToRemoteMcpServersOverHttp) } - MCPImportFeedback(report: importReport, problem: importProblem) + if importReport != nil || importProblem != nil { + MCPImportFeedback(report: importReport, problem: importProblem) + } } .navigationTitle(AppStrings.Settings.mcp.swiftUIKey) .toolbar { diff --git a/TFI/Features/Settings/MobileSkillSettings.swift b/TFI/Features/Settings/MobileSkillSettings.swift index a1ac5915..25b746ae 100644 --- a/TFI/Features/Settings/MobileSkillSettings.swift +++ b/TFI/Features/Settings/MobileSkillSettings.swift @@ -58,10 +58,6 @@ struct MobileSkillSettings: View { } } } - } footer: { - Text( - AppStrings.Settings.skillExplanation - ) } ForEach(warnings, id: \.self) { Text($0).font(.caption).foregroundStyle(.orange) } if let error = error ?? skills.storageError { Text(error).foregroundStyle(.red) } diff --git a/TFITests/MobileAISettingsRenderingTests.swift b/TFITests/MobileAISettingsRenderingTests.swift index 44f3d36d..5481dab3 100644 --- a/TFITests/MobileAISettingsRenderingTests.swift +++ b/TFITests/MobileAISettingsRenderingTests.swift @@ -12,6 +12,34 @@ import UIKit /// Uses the shipping views and a disposable in-memory app, without accounts or network requests. @Suite(.serialized) @MainActor struct MobileAISettingsRenderingTests { + @Test( + .enabled(if: ProcessInfo.processInfo.environment["TERMIND_AI_SCREENSHOTS"] != nil), + arguments: ["en", "zh-Hans", "zh-Hant"]) + func simplifiedSettingsRenderWithoutFootnotes(_ language: String) async throws { + let app = try MobileAppModel(inMemory: true) + let ai = AIConfigurationModel(persistsSecrets: false) + let locale = Locale(identifier: language) + try await capture( + "mcp-empty-without-footer", language: language, + view: NavigationStack { MobileMCPSettings(ai: ai, pro: app.pro) }.environment(\.locale, locale)) + ai.upsert(MCPServer(name: "Documentation", url: "https://example.invalid/mcp")) + try await capture( + "mcp-list-without-footer", language: language, + view: NavigationStack { MobileMCPSettings(ai: ai, pro: app.pro) }.environment(\.locale, locale)) + try await capture( + "skills-without-footer", language: language, + view: NavigationStack { MobileSkillSettings(skills: app.skills) }.environment(\.locale, locale)) + try await capture( + "provider-without-footer", language: language, + view: MobileProviderEditor(ai: ai, provider: ModelProvider(name: "")) + .environment(\.locale, locale)) + try await capture( + "support-link-rows", language: language, + view: NavigationStack { + MobileAppSettingsView(workspaceSettings: app.workspaceSettings) + }.environment(\.locale, locale), scrollToBottom: true) + } + @Test( .enabled(if: ProcessInfo.processInfo.environment["TERMIND_AI_SCREENSHOTS"] != nil), arguments: ["en", "zh-Hans", "zh-Hant"] @@ -249,7 +277,10 @@ struct MobileAISettingsRenderingTests { let directory = FileManager.default.temporaryDirectory.appending(path: "ai-settings-screenshots") try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let device = UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone" - if ["provider-hints", "provider-vertex"].contains(name) { + if [ + "provider-hints", "provider-vertex", "mcp-empty-without-footer", "mcp-list-without-footer", + "skills-without-footer", "provider-without-footer", "support-link-rows", + ].contains(name) { Attachment.record([UInt8](data), named: "\(device)-\(language)-\(name).png") } try data.write(to: directory.appending(path: "\(device)-\(language)-\(name).png")) diff --git a/TFM/Features/Settings/MCPServerSettings.swift b/TFM/Features/Settings/MCPServerSettings.swift index 5e2cedc3..37aece01 100644 --- a/TFM/Features/Settings/MCPServerSettings.swift +++ b/TFM/Features/Settings/MCPServerSettings.swift @@ -11,7 +11,7 @@ struct MCPServerSettings: View { private var server: MCPServerController { app.mcpServer } var body: some View { - Section(AppStrings.Settings.server.swiftUIKey) { + Section(AppStrings.Settings.builtInServer.swiftUIKey) { Toggle( AppStrings.Settings.enable.swiftUIKey, isOn: Binding( diff --git a/TFM/Features/Settings/MCPSettings.swift b/TFM/Features/Settings/MCPSettings.swift index 9b826db7..d5416b4b 100644 --- a/TFM/Features/Settings/MCPSettings.swift +++ b/TFM/Features/Settings/MCPSettings.swift @@ -9,8 +9,6 @@ import UniformTypeIdentifiers /// /// HTTP only, and that is a property of the app rather than a shortcut — Termind is /// sandboxed, so it cannot launch the local process a stdio server would need. -/// The footnote says so, because "why is there no stdio option" is otherwise a -/// reasonable thing to wonder for a while. struct MCPSettings: View { @Environment(AppModel.self) private var app let edit: (MCPServer.ID, Bool) -> Void @@ -40,12 +38,9 @@ struct MCPSettings: View { } Button(AppStrings.MCP.importServers.swiftUIKey) { importMCP() } - MCPImportFeedback(report: importReport, problem: importProblem) - - Text(AppStrings.Settings.termindConnectsDirectlyFromThisMacOverHttp) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) + if importReport != nil || importProblem != nil { + MCPImportFeedback(report: importReport, problem: importProblem) + } if let error = app.aiConfiguration.storageError { Label(error, systemImage: "exclamationmark.triangle.fill") @@ -53,11 +48,13 @@ struct MCPSettings: View { .foregroundStyle(.orange) .fixedSize(horizontal: false, vertical: true) } - - FormNavigationAction(title: AppStrings.Settings.server, action: openServer) } header: { Text(AppStrings.Settings.mcp) } + + Section { + FormNavigationAction(title: AppStrings.Settings.builtInServer, action: openServer) + } } private func importMCP() { diff --git a/TFM/Features/Settings/ModelProviderEditorSheet.swift b/TFM/Features/Settings/ModelProviderEditorSheet.swift index fb4e4912..59c504ae 100644 --- a/TFM/Features/Settings/ModelProviderEditorSheet.swift +++ b/TFM/Features/Settings/ModelProviderEditorSheet.swift @@ -136,13 +136,6 @@ struct ModelProviderEditorPage: View { ), prompt: Text(AppStrings.Common.optional) ) - - Text( - AppStrings.Settings.theKeyIsStoredInThisMacSDataProtectionKeychain - ) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) } } diff --git a/TFM/Features/Settings/ModelProviderSettings.swift b/TFM/Features/Settings/ModelProviderSettings.swift index b3e4567f..e59bcd70 100644 --- a/TFM/Features/Settings/ModelProviderSettings.swift +++ b/TFM/Features/Settings/ModelProviderSettings.swift @@ -4,12 +4,6 @@ import TermindDomain import TermindPresentation /// The "Model Providers" section of the Settings page. -/// -/// A row per endpoint, plus the one line of small print that matters most here: -/// requests go from this Mac to the address in that row, and nowhere else. That -/// sentence is the product, not a disclaimer — an SSH client that shipped -/// terminal output through somebody else's server would be a different and much -/// worse thing. struct ModelProviderSettings: View { @Environment(AppModel.self) private var app let edit: (ModelProvider.ID, Bool) -> Void @@ -33,13 +27,6 @@ struct ModelProviderSettings: View { } } - Text( - AppStrings.Settings.directProviderConnectionExplanation - ) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - if let error = app.aiConfiguration.storageError { Label(error, systemImage: "exclamationmark.triangle.fill") .font(.caption) diff --git a/TFM/Features/Settings/SettingsPageView.swift b/TFM/Features/Settings/SettingsPageView.swift index 3e8ec50e..009b338e 100644 --- a/TFM/Features/Settings/SettingsPageView.swift +++ b/TFM/Features/Settings/SettingsPageView.swift @@ -537,7 +537,10 @@ private struct AppInfoSettings: View { AppReviewLink(controller: app.appReview) { Label(AppStrings.Settings.rateOnTheAppStore.swiftUIKey, systemImage: "star") .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(.rect) } + .buttonStyle(.plain) + .foregroundStyle(.tint) Link(destination: URL(string: "https://termind.akinokaede.com/support/")!) { Label(AppStrings.Settings.support.swiftUIKey, systemImage: "questionmark.circle") .frame(maxWidth: .infinity, alignment: .leading) @@ -559,20 +562,12 @@ private struct StartupSettings: View { var body: some View { Section(AppStrings.Settings.startup.swiftUIKey) { Toggle( + AppStrings.Settings.openAtLogin.swiftUIKey, isOn: Binding( get: { loginItem.isEnabled }, set: { loginItem.setEnabled($0) } ) - ) { - VStack(alignment: .leading, spacing: Metrics.Space.xxs) { - Text(AppStrings.Settings.openAtLogin) - Text( - AppStrings.Settings.loginItemExplanation - ) - .font(.caption) - .foregroundStyle(.secondary) - } - } + ) Toggle(AppStrings.Settings.startMinimizedToSystemTray.swiftUIKey, isOn: $opensQuietly) .disabled(!loginItem.isEnabled) diff --git a/TFM/Features/Settings/SkillSettings.swift b/TFM/Features/Settings/SkillSettings.swift index f6a4d7d7..6c91c726 100644 --- a/TFM/Features/Settings/SkillSettings.swift +++ b/TFM/Features/Settings/SkillSettings.swift @@ -5,12 +5,6 @@ import TermindPresentation import UniformTypeIdentifiers /// The "Skills" section: procedures the assistant may load and follow. -/// -/// The footnote is doing real work. A skill is the one thing in Termind whose -/// text the model treats as instructions rather than data, so the page has to -/// say both halves of that: it is followed, and it still cannot approve -/// anything. Someone importing a file from the internet should read that before -/// they enable it, not after. struct SkillSettings: View { @Environment(AppModel.self) private var app let edit: (AgentSkill.ID, Bool) -> Void @@ -39,13 +33,6 @@ struct SkillSettings: View { Button(AppStrings.Settings.importSkill.swiftUIKey) { importSkill() } - Text( - AppStrings.Settings.skillExplanation - ) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - ForEach(importWarnings, id: \.self) { warning in Label(warning, systemImage: "exclamationmark.triangle.fill") .font(.caption) diff --git a/TFM/Features/Settings/WebSearchSettings.swift b/TFM/Features/Settings/WebSearchSettings.swift index a295a135..cb74c7c5 100644 --- a/TFM/Features/Settings/WebSearchSettings.swift +++ b/TFM/Features/Settings/WebSearchSettings.swift @@ -25,14 +25,8 @@ struct WebSearchSettings: View { Text( usesBuiltIn - ? String( - localized: - AppStrings.Settings.nativeAndFallbackSearchExplanation - ) - : String( - localized: - AppStrings.Settings.webSearchExplanation - ) + ? AppStrings.Settings.nativeAndFallbackSearchExplanation + : AppStrings.Settings.webSearchExplanation ) .font(.caption) .foregroundStyle(.secondary) diff --git a/TFMTests/AppLanguageTests.swift b/TFMTests/AppLanguageTests.swift index 95d84269..ddb9920c 100644 --- a/TFMTests/AppLanguageTests.swift +++ b/TFMTests/AppLanguageTests.swift @@ -203,6 +203,36 @@ struct AppLanguageTests { #expect(AppLocalization.string("Settings", locale: Locale(identifier: "zh-Hant")) == "設定") } + @Test(arguments: [ + ("en", "Built-in Server"), + ("zh-Hans", "内置服务器"), + ("zh-Hant", "內建伺服器"), + ("de", "Integrierter Server"), + ("fr", "Serveur intégré"), + ("ja", "組み込みサーバ"), + ("ko", "내장 서버"), + ]) + func builtInServerTitleIsLocalizedInEverySupportedLanguage(identifier: String, expected: String) { + #expect( + AppLocalization.resolve( + AppStrings.Settings.builtInServer, locale: Locale(identifier: identifier)) == expected) + } + + @Test(arguments: [ + ("en", "Minimum Duration (s)"), + ("zh-Hans", "最短运行时间(秒)"), + ("zh-Hant", "最短執行時間(秒)"), + ("de", "Mindestdauer (s)"), + ("fr", "Durée minimale (s)"), + ("ja", "最短実行時間(秒)"), + ("ko", "최소 실행 시간 (초)"), + ]) + func minimumDurationLocalizesTheWholeLabel(identifier: String, expected: String) { + #expect( + AppLocalization.resolve( + AppStrings.CommandNotifications.minimumDuration, locale: Locale(identifier: identifier)) == expected) + } + @Test(arguments: [ (AppLanguage.english, "The new language will be used after Termind relaunches."), (.japanese, "新しい言語はTermindの再起動後に適用されます。"), diff --git a/TFMTests/Screenshots/SettingsCopyRenderingTests.swift b/TFMTests/Screenshots/SettingsCopyRenderingTests.swift new file mode 100644 index 00000000..5046608c --- /dev/null +++ b/TFMTests/Screenshots/SettingsCopyRenderingTests.swift @@ -0,0 +1,78 @@ +import AgentKit +import AppKit +import SwiftUI +import TermindApplication +import Testing + +@testable import Termind + +@Suite(.requiresVisibleWindow, .serialized) @MainActor +struct SettingsCopyRenderingTests { + @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + func builtInServerHasConsistentTitlesWithoutTransportFootnote(_ language: String) async throws { + let app = AppModel(hosts: HostLibraryModel(), ai: AIConfigurationModel(persistsSecrets: false)) + defer { app.shutdownForTermination() } + try await capture( + Form { MCPSettings(edit: { _, _ in }, openServer: {}) } + .formStyle(.grouped).environment(app), + language: language, name: "mcp-settings") + try await capture( + Form { MCPServerSettings() }.formStyle(.grouped).environment(app), + language: language, name: "built-in-server") + } + + @Test(arguments: ["en", "zh-Hans", "zh-Hant"]) + func simplifiedSettingsRenderWithoutEmptyRows(_ language: String) async throws { + let app = AppModel(hosts: HostLibraryModel(), ai: AIConfigurationModel(persistsSecrets: false)) + defer { app.shutdownForTermination() } + try await capture( + Form { + ModelProviderSettings(edit: { _, _ in }) + SkillSettings(edit: { _, _ in }) + WebSearchSettings() + }.formStyle(.grouped).environment(app), + language: language, name: "ai-settings") + var search = app.aiConfiguration.webSearch + search.prefersBuiltIn.toggle() + app.aiConfiguration.setWebSearch(search) + try await capture( + Form { WebSearchSettings() }.formStyle(.grouped).environment(app), + language: language, name: "web-search-alternate") + + let window = WindowModel(workspaces: []) + let provider = ModelProvider(name: "") + #expect(app.aiConfiguration.saveProvider(provider, endpoint: .init())) + try await capture( + ModelProviderEditorPage(modelProviderID: provider.id, isNew: true, showModels: {}, deleted: {}) + .environment(app).environment(window), + language: language, name: "provider-without-footer") + for category: SettingsCategory in [.app, .connections, .notifications] { + window.openSettings(route: .category(category)) + try await capture( + SettingsPageView().environment(app).environment(window), + language: language, name: "settings-\(category)", height: category == .app ? 1700 : 1000) + } + } + + private func capture(_ view: some View, language: String, name: String, height: CGFloat = 700) async throws { + let content = view.environment(\.locale, Locale(identifier: language)).frame(width: 680, height: height) + let controller = NSHostingController(rootView: content) + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 680, height: height), + styleMask: [.titled], backing: .buffered, defer: false) + window.isReleasedWhenClosed = false + window.contentViewController = controller + window.orderFront(nil) + defer { + window.orderOut(nil) + window.contentViewController = nil + window.close() + } + try await Task.sleep(for: .milliseconds(600)) + controller.view.layoutSubtreeIfNeeded() + let bitmap = try #require(controller.view.bitmapImageRepForCachingDisplay(in: controller.view.bounds)) + controller.view.cacheDisplay(in: controller.view.bounds, to: bitmap) + let data = try #require(bitmap.representation(using: .png, properties: [:])) + Attachment.record(data, named: "\(name)-\(language).png") + } +}