diff --git a/Assets.xcassets/CloudWelcomeIcon.imageset/CloudWelcomeIcon.png b/Assets.xcassets/CloudWelcomeIcon.imageset/CloudWelcomeIcon.png new file mode 100644 index 000000000000..e8452e0e7eda Binary files /dev/null and b/Assets.xcassets/CloudWelcomeIcon.imageset/CloudWelcomeIcon.png differ diff --git a/Assets.xcassets/CloudWelcomeIcon.imageset/Contents.json b/Assets.xcassets/CloudWelcomeIcon.imageset/Contents.json new file mode 100644 index 000000000000..be724154aeeb --- /dev/null +++ b/Assets.xcassets/CloudWelcomeIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "CloudWelcomeIcon.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/CloudWelcome/spin-up.mp4 b/Resources/CloudWelcome/spin-up.mp4 index b03c78ce9d20..d71184c162fe 100644 Binary files a/Resources/CloudWelcome/spin-up.mp4 and b/Resources/CloudWelcome/spin-up.mp4 differ diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index da416e97e90e..70efa0096459 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -313822,65 +313822,6 @@ } } }, - "settings.betaFeatures.clickByHint": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "النقر عبر التلميح" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Per Hinweis klicken" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Click by Hint" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Clic por indicación" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cliquer par repère" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ヒントでクリック" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "힌트로 클릭" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "按提示点击" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "依提示點按" - } - } - } - }, "settings.betaFeatures.feed": { "extractionState": "manual", "localizations": { @@ -378644,125 +378585,6 @@ } } }, - "shortcut.focusPaneByNumber.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "التركيز على اللوحة 1…9" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Fokusiraj panel 1…9" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Fokuser panel 1…9" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Bereich 1…9 fokussieren" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Focus Pane 1…9" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enfocar panel 1…9" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Activer le panneau 1…9" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Sposta focus sul pannello 1…9" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ペイン 1…9 にフォーカス" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "패널 1…9로 포커스" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Fokuser panel 1…9" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Fokus na panel 1…9" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Focar Painel 1…9" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Фокус на панель 1…9" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "โฟกัสบานหน้าต่าง 1…9" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Bölme 1…9'a Odaklan" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Фокус на панель 1…9" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "聚焦窗格 1…9" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "聚焦窗格 1…9" - } - } - } - }, "shortcut.focusPaneDown.label": { "extractionState": "manual", "localizations": { @@ -379417,482 +379239,6 @@ } } }, - "featureFlags.windowHints.title": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "النقر عبر التلميح" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Klik putem oznake" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Klik via mærkat" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Per Hinweis klicken" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Click by Hint" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Clic por indicación" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cliquer par repère" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Clic tramite etichetta" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ヒントでクリック" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "힌트로 클릭" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Klikk via merke" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kliknij przez etykietę" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Clicar por dica" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Нажатие по подсказке" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คลิกด้วยคำใบ้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "İpucuyla tıkla" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Натискання за підказкою" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "按提示点击" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "依提示點按" - } - } - } - }, - "featureFlags.windowHints.description": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يضع تسمية على كل عنصر تحكم قابل للنقر في النافذة، فتؤدي كتابة التسمية إلى النقر عليه." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Označava svaku kontrolu u prozoru na koju se može kliknuti, pa kucanje oznake klikne na nju." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Mærker hver klikbar kontrol i vinduet, så det at skrive et mærkat klikker på den." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Beschriftet jedes anklickbare Steuerelement im Fenster, sodass das Eingeben einer Beschriftung es anklickt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Labels every clickable control in the window so typing a label clicks it." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Etiqueta cada control en el que se puede hacer clic en la ventana para que escribir una etiqueta lo pulse." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Étiquette chaque contrôle cliquable de la fenêtre ; taper une étiquette clique dessus." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Etichetta ogni controllo cliccabile nella finestra, così digitare un'etichetta lo clicca." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ウィンドウ内のクリックできるコントロールすべてにラベルを付け、ラベルを入力するとそのコントロールをクリックします。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "창에서 클릭할 수 있는 모든 컨트롤에 레이블을 붙이고, 레이블을 입력하면 해당 컨트롤을 클릭합니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Merker hver klikkbare kontroll i vinduet, slik at du klikker den ved å skrive merket." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Oznacza każdą klikalną kontrolkę w oknie, aby wpisanie etykiety ją klikało." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Rotula cada controle clicável da janela para que digitar um rótulo clique nele." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Помечает каждый кликабельный элемент управления в окне, чтобы ввод метки нажимал на него." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ติดป้ายกำกับให้ทุกตัวควบคุมที่คลิกได้ในหน้าต่าง เพื่อให้การพิมพ์ป้ายกำกับคลิกตัวควบคุมนั้น" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Penceredeki tıklanabilir her denetimi etiketler; bir etiketi yazmak o denetime tıklar." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Позначає кожен елемент керування у вікні, на який можна натиснути, щоб введення мітки натискало на нього." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "为窗口中每个可点击的控件添加标签,输入标签即可点击该控件。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "為視窗中每個可點按的控制項加上標籤,輸入標籤即可點按該控制項。" - } - } - } - }, - "shortcut.showWindowHints.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "النقر عبر التلميح" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Klik putem oznake" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Klik via mærkat" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Per Hinweis klicken" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Click by Hint" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Clic por indicación" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Cliquer par repère" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Clic tramite etichetta" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ヒントでクリック" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "힌트로 클릭" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Klikk via merke" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kliknij przez etykietę" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Clicar por dica" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Нажатие по подсказке" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "คลิกด้วยคำใบ้" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "İpucuyla tıkla" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Натискання за підказкою" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "按提示点击" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "依提示點按" - } - } - } - }, - "shortcut.focusSidebar.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "التركيز على الشريط الجانبي" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Fokusiraj bočnu traku" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Fokuser sidebjælken" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Seitenleiste fokussieren" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Focus Sidebar" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Enfocar barra lateral" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Activer la barre latérale" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Sposta focus sulla barra laterale" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サイドバーにフォーカス" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사이드바로 포커스" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Fokuser sidepanelet" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Fokus na pasek boczny" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Focar Barra Lateral" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Фокус на боковую панель" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "โฟกัสแถบด้านข้าง" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Kenar Çubuğuna Odaklan" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Фокус на бічну панель" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "聚焦侧边栏" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "聚焦側邊欄" - } - } - } - }, "shortcut.focusTextBoxInput.label": { "extractionState": "manual", "localizations": { @@ -383229,125 +382575,6 @@ } } }, - "shortcut.nextAgentTurn.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "دور الوكيل التالي" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Sljedeći potez agenta" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Næste agenttur" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Nächster Agent-Durchgang" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Next Agent Turn" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Turno siguiente del agente" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Tour suivant de l’agent" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Turno successivo dell’agente" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "次のエージェントのターン" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다음 에이전트 턴" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Neste agenttur" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Następna tura agenta" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Próximo Turno do Agente" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Следующий ход агента" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รอบถัดไปของเอเจนต์" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sonraki Ajan Turu" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Наступний хід агента" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "下一个代理轮次" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "下一個代理回合" - } - } - } - }, "shortcut.nextSurface.label": { "extractionState": "manual", "localizations": { @@ -384127,125 +383354,6 @@ } } }, - "shortcut.previousAgentTurn.label": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "دور الوكيل السابق" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Prethodni potez agenta" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Forrige agenttur" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Vorheriger Agent-Durchgang" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Previous Agent Turn" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Turno anterior del agente" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Tour précédent de l’agent" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Turno precedente dell’agente" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "前のエージェントのターン" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이전 에이전트 턴" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Forrige agenttur" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Poprzednia tura agenta" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Turno Anterior do Agente" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Предыдущий ход агента" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รอบก่อนหน้าของเอเจนต์" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Önceki Ajan Turu" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Попередній хід агента" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "上一个代理轮次" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "上一個代理回合" - } - } - } - }, "shortcut.previousSurface.label": { "extractionState": "manual", "localizations": { @@ -595238,244 +594346,6 @@ } } }, - "agentTurnRail.accessibilityLabel": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "أدوار الوكيل" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Potezi agenta" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Agentens ture" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Agent-Durchgänge" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Agent turns" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Turnos del agente" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Tours de l’agent" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Turni dell’agente" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エージェントのターン" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "에이전트 턴" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Agentens turer" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Tury agenta" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Turnos do agente" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Ходы агента" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "รอบของเอเจนต์" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Ajan turları" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Ходи агента" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "代理轮次" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "代理回合" - } - } - } - }, - "agentTurnRail.notInScrollback": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لم يعد في سجل التمرير" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Više nije u historiji pomicanja" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Ikke længere i rullehistorikken" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Nicht mehr im Scrollback" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "No longer in scrollback" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Ya no está en el historial de desplazamiento" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "N’est plus dans l’historique de défilement" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Non più nello scrollback" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "スクロールバックにありません" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "스크롤백에 더 이상 없음" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Ikke lenger i rullehistorikken" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Już poza historią przewijania" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Não está mais no histórico de rolagem" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Больше нет в истории прокрутки" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ไม่อยู่ในประวัติการเลื่อนแล้ว" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Artık kaydırma geçmişinde değil" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Більше немає в історії прокручування" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已不在回滚记录中" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "已不在捲動記錄中" - } - } - } - }, "agentWake.banner.close": { "extractionState": "manual", "localizations": { @@ -646373,65 +645243,6 @@ } } }, - "cloud.welcome.slide.displays.caption": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Open a display next to your terminal and watch apps and browsers run." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Öffne ein Display neben deinem Terminal und sieh Apps und Browsern bei der Arbeit zu." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ouvrez un écran à côté de votre terminal et regardez les apps et les navigateurs tourner." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "افتح شاشة بجانب الطرفية وشاهد التطبيقات والمتصفحات وهي تعمل." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Abre una pantalla junto a tu terminal y mira cómo se ejecutan apps y navegadores." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "在終端機旁開啟顯示器,看著 App 和瀏覽器執行。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在终端旁打开显示器,看着应用和浏览器运行。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "터미널 옆에 디스플레이를 열고 앱과 브라우저가 실행되는 모습을 확인하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ターミナルの横にディスプレイを開いて、アプリやブラウザの動作を確認できます。" - } - } - } - }, "cloud.welcome.slide.displays.title": { "extractionState": "manual", "localizations": { @@ -646491,65 +645302,6 @@ } } }, - "cloud.welcome.slide.invite.caption": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Add teammates by email so they can use your team’s Cloud machines." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Füge Teammitglieder per E-Mail hinzu, damit sie die Cloud-Maschinen deines Teams nutzen können." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ajoutez des coéquipiers par e-mail pour qu’ils puissent utiliser les machines Cloud de votre équipe." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "أضف زملاءك عبر البريد الإلكتروني ليتمكنوا من استخدام أجهزة Cloud الخاصة بفريقك." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Agrega a tus compañeros por correo para que puedan usar las máquinas Cloud de tu equipo." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "透過電子郵件新增團隊成員,讓他們使用團隊的 Cloud 機器。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "通过电子邮件添加团队成员,让他们使用团队的 Cloud 机器。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "이메일로 팀원을 추가하면 팀의 Cloud 머신을 함께 사용할 수 있습니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "メールでチームメンバーを追加すると、チームの Cloud マシンを使えるようになります。" - } - } - } - }, "cloud.welcome.slide.invite.title": { "extractionState": "manual", "localizations": { @@ -646609,65 +645361,6 @@ } } }, - "cloud.welcome.slide.keepsRunning.caption": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Agents and terminals keep running after you close your laptop. Use the cmux CLI to connect local agents to your Cloud machines." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Agenten und Terminals laufen weiter, nachdem du deinen Laptop geschlossen hast. Mit der cmux-CLI kannst du deine lokalen Agenten mit deinen Cloud-Maschinen verbinden." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les agents et les terminaux continuent de fonctionner après la fermeture de votre ordinateur portable. Utilisez la CLI cmux pour connecter vos agents locaux à vos machines Cloud." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تستمر الوكلاء والطرفيات في العمل بعد إغلاق حاسوبك المحمول. استخدم cmux CLI لتوصيل وكلائك المحليين بأجهزتك السحابية." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Los agentes y terminales siguen funcionando después de cerrar el portátil. Usa la CLI de cmux para conectar tus agentes locales a tus máquinas Cloud." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "關閉筆電後,代理程式和終端機仍會持續執行。使用 cmux CLI,將你的本機代理程式連接到 Cloud 機器。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "合上笔记本电脑后,代理和终端仍会继续运行。使用 cmux CLI,将您的本地代理连接到 Cloud 机器。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "노트북을 닫아도 에이전트와 터미널이 계속 실행됩니다. cmux CLI로 로컬 에이전트를 Cloud 머신에 연결할 수 있습니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ノートパソコンを閉じてもエージェントとターミナルは動き続けます。cmux CLI でローカルエージェントを Cloud マシンに接続できます。" - } - } - } - }, "cloud.welcome.slide.keepsRunning.title": { "extractionState": "manual", "localizations": { @@ -646727,65 +645420,6 @@ } } }, - "cloud.welcome.slide.spinUp.caption": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Each one opens as a regular cmux workspace with a terminal ready." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Jede Maschine wird als regulärer cmux-Arbeitsbereich mit bereitstehendem Terminal geöffnet." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Chaque machine s’ouvre comme un espace de travail cmux classique avec un terminal prêt." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تفتح كل واحدة كمساحة عمل cmux عادية مع طرفية جاهزة." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cada una se abre como un espacio de trabajo normal de cmux con un terminal listo." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "每台機器都會以一般 cmux 工作區開啟,並準備好終端機。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "每台机器都会作为普通 cmux 工作区打开,并准备好终端。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "각 머신은 터미널이 준비된 일반 cmux 워크스페이스로 열립니다." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "それぞれがターミナルを備えた通常の cmux ワークスペースとして開きます。" - } - } - } - }, "cloud.welcome.slide.spinUp.title": { "extractionState": "manual", "localizations": { @@ -646845,65 +645479,6 @@ } } }, - "cloud.welcome.slide.team.caption": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Invite teammates to the same machine and collaborate in a shared workspace." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Lade Teammitglieder auf dieselbe Maschine ein und arbeitet in einem gemeinsamen Workspace." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Invitez vos coéquipiers sur la même machine et collaborez dans un espace de travail partagé." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "ادعُ زملاءك إلى الجهاز نفسه وتعاونوا في مساحة عمل مشتركة." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Invita a tu equipo a la misma máquina y colaboren en un espacio de trabajo compartido." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "邀請團隊成員使用同一台機器,在共享工作區中協作。" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "邀请团队成员使用同一台机器,在共享工作区中协作。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "팀원을 같은 머신에 초대하고 공유 워크스페이스에서 협업하세요." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "チームメンバーを同じマシンに招待して、共有ワークスペースで共同作業できます。" - } - } - } - }, "cloud.welcome.slide.team.title": { "extractionState": "manual", "localizations": { @@ -647199,124 +645774,6 @@ } } }, - "debug.menu.showCloudWelcomeList": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Show Cloud Welcome (List)…" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-Willkommen (Liste) anzeigen…" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher l’accueil Cloud (liste)…" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إظهار ترحيب Cloud (قائمة)…" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar bienvenida de Cloud (lista)…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "顯示 Cloud 歡迎畫面(列表)…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示 Cloud 欢迎界面(列表)…" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud 환영 화면 보기 (목록)…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud のようこそ画面を表示(リスト)…" - } - } - } - }, - "debug.menu.showCloudWelcomeListDots": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Show Cloud Welcome (List + Dots)…" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Cloud-Willkommen (Liste + Punkte) anzeigen…" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher l’accueil Cloud (liste + points)…" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إظهار ترحيب Cloud (قائمة ونقاط)…" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar bienvenida de Cloud (lista y puntos)…" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "顯示 Cloud 歡迎畫面(列表+圓點)…" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "显示 Cloud 欢迎界面(列表和圆点)…" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Cloud 환영 화면 보기 (목록 + 점)…" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Cloud のようこそ画面を表示(リスト+ドット)…" - } - } - } - }, "programStatus.state.needsPermission": { "extractionState": "manual", "localizations": { @@ -649305,6 +647762,1195 @@ } } }, + "ssh.tui.browserForward.ended": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The SSH port forward ended before it was ready. Reload to reconnect." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "انتهت إعادة توجيه منفذ SSH قبل أن تصبح جاهزة. أعد التحميل لإعادة الاتصال." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die SSH-Portweiterleitung wurde beendet, bevor sie bereit war. Neu laden, um die Verbindung wiederherzustellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El reenvío de puerto SSH terminó antes de estar listo. Recarga para volver a conectar." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La redirection de port SSH s’est arrêtée avant d’être prête. Rechargez pour vous reconnecter." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "SSH ポート転送が準備完了前に終了しました。再読み込みして再接続してください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "SSH 포트 포워딩이 준비되기 전에 종료되었습니다. 다시 연결하려면 새로고침하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "SSH 端口转发在就绪前已结束。请重新加载以重新连接。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "SSH 連接埠轉送在就緒前已結束。請重新載入以重新連線。" + } + } + } + }, + "ssh.tui.browserForward.httpsUnsupported": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HTTPS services on an SSH host can't open in the browser yet. Use http:// instead." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لا يمكن فتح خدمات HTTPS على مضيف SSH في المتصفح بعد. استخدم http:// بدلاً من ذلك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "HTTPS-Dienste auf einem SSH-Host können noch nicht im Browser geöffnet werden. Verwende stattdessen http://." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los servicios HTTPS de un host SSH aún no se pueden abrir en el navegador. Usa http:// en su lugar." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les services HTTPS d’un hôte SSH ne peuvent pas encore s’ouvrir dans le navigateur. Utilisez http:// à la place." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "SSH ホスト上の HTTPS サービスはまだブラウザで開けません。代わりに http:// を使用してください。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "SSH 호스트의 HTTPS 서비스는 아직 브라우저에서 열 수 없습니다. 대신 http://를 사용하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "SSH 主机上的 HTTPS 服务暂时无法在浏览器中打开。请改用 http://。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "SSH 主機上的 HTTPS 服務暫時無法在瀏覽器中開啟。請改用 http://。" + } + } + } + }, + "shortcut.focusPaneByNumber.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "التركيز على اللوحة 1…9" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Fokusiraj panel 1…9" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Fokuser panel 1…9" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereich 1…9 fokussieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Focus Pane 1…9" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Enfocar panel 1…9" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer le panneau 1…9" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta focus sul pannello 1…9" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ペイン 1…9 にフォーカス" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "패널 1…9로 포커스" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fokuser panel 1…9" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Fokus na panel 1…9" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Focar Painel 1…9" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Фокус на панель 1…9" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โฟกัสบานหน้าต่าง 1…9" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Bölme 1…9'a Odaklan" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Фокус на панель 1…9" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "聚焦窗格 1…9" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "聚焦窗格 1…9" + } + } + } + }, + "featureFlags.windowHints.title": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "النقر عبر التلميح" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Klik putem oznake" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Klik via mærkat" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Per Hinweis klicken" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Click by Hint" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clic por indicación" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cliquer par repère" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Clic tramite etichetta" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヒントでクリック" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "힌트로 클릭" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Klikk via merke" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kliknij przez etykietę" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Clicar por dica" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Нажатие по подсказке" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คลิกด้วยคำใบ้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İpucuyla tıkla" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Натискання за підказкою" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "按提示点击" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "依提示點按" + } + } + } + }, + "featureFlags.windowHints.description": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يضع تسمية على كل عنصر تحكم قابل للنقر في النافذة، فتؤدي كتابة التسمية إلى النقر عليه." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Označava svaku kontrolu u prozoru na koju se može kliknuti, pa kucanje oznake klikne na nju." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Mærker hver klikbar kontrol i vinduet, så det at skrive et mærkat klikker på den." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschriftet jedes anklickbare Steuerelement im Fenster, sodass das Eingeben einer Beschriftung es anklickt." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Labels every clickable control in the window so typing a label clicks it." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Etiqueta cada control en el que se puede hacer clic en la ventana para que escribir una etiqueta lo pulse." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étiquette chaque contrôle cliquable de la fenêtre ; taper une étiquette clique dessus." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Etichetta ogni controllo cliccabile nella finestra, così digitare un'etichetta lo clicca." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ウィンドウ内のクリックできるコントロールすべてにラベルを付け、ラベルを入力するとそのコントロールをクリックします。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "창에서 클릭할 수 있는 모든 컨트롤에 레이블을 붙이고, 레이블을 입력하면 해당 컨트롤을 클릭합니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Merker hver klikkbare kontroll i vinduet, slik at du klikker den ved å skrive merket." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Oznacza każdą klikalną kontrolkę w oknie, aby wpisanie etykiety ją klikało." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Rotula cada controle clicável da janela para que digitar um rótulo clique nele." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Помечает каждый кликабельный элемент управления в окне, чтобы ввод метки нажимал на него." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ติดป้ายกำกับให้ทุกตัวควบคุมที่คลิกได้ในหน้าต่าง เพื่อให้การพิมพ์ป้ายกำกับคลิกตัวควบคุมนั้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Penceredeki tıklanabilir her denetimi etiketler; bir etiketi yazmak o denetime tıklar." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Позначає кожен елемент керування у вікні, на який можна натиснути, щоб введення мітки натискало на нього." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "为窗口中每个可点击的控件添加标签,输入标签即可点击该控件。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "為視窗中每個可點按的控制項加上標籤,輸入標籤即可點按該控制項。" + } + } + } + }, + "shortcut.showWindowHints.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "النقر عبر التلميح" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Klik putem oznake" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Klik via mærkat" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Per Hinweis klicken" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Click by Hint" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clic por indicación" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cliquer par repère" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Clic tramite etichetta" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヒントでクリック" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "힌트로 클릭" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Klikk via merke" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kliknij przez etykietę" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Clicar por dica" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Нажатие по подсказке" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "คลิกด้วยคำใบ้" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "İpucuyla tıkla" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Натискання за підказкою" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "按提示点击" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "依提示點按" + } + } + } + }, + "shortcut.focusSidebar.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "التركيز على الشريط الجانبي" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Fokusiraj bočnu traku" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Fokuser sidebjælken" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Seitenleiste fokussieren" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Focus Sidebar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Enfocar barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer la barre latérale" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta focus sulla barra laterale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーにフォーカス" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사이드바로 포커스" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Fokuser sidepanelet" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Fokus na pasek boczny" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Focar Barra Lateral" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Фокус на боковую панель" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "โฟกัสแถบด้านข้าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Kenar Çubuğuna Odaklan" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Фокус на бічну панель" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "聚焦侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "聚焦側邊欄" + } + } + } + }, + "shortcut.nextAgentTurn.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "دور الوكيل التالي" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Sljedeći potez agenta" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Næste agenttur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächster Agent-Durchgang" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Next Agent Turn" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Turno siguiente del agente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour suivant de l’agent" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Turno successivo dell’agente" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "次のエージェントのターン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다음 에이전트 턴" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Neste agenttur" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Następna tura agenta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Próximo Turno do Agente" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Следующий ход агента" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รอบถัดไปของเอเจนต์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sonraki Ajan Turu" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Наступний хід агента" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "下一个代理轮次" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "下一個代理回合" + } + } + } + }, + "shortcut.previousAgentTurn.label": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "دور الوكيل السابق" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Prethodni potez agenta" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Forrige agenttur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorheriger Agent-Durchgang" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Previous Agent Turn" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Turno anterior del agente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tour précédent de l’agent" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Turno precedente dell’agente" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "前のエージェントのターン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이전 에이전트 턴" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Forrige agenttur" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Poprzednia tura agenta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Turno Anterior do Agente" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Предыдущий ход агента" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รอบก่อนหน้าของเอเจนต์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Önceki Ajan Turu" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Попередній хід агента" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "上一个代理轮次" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "上一個代理回合" + } + } + } + }, + "agentTurnRail.accessibilityLabel": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "أدوار الوكيل" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Potezi agenta" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Agentens ture" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent-Durchgänge" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Agent turns" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Turnos del agente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tours de l’agent" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Turni dell’agente" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントのターン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트 턴" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Agentens turer" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Tury agenta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Turnos do agente" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Ходы агента" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "รอบของเอเจนต์" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Ajan turları" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Ходи агента" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "代理轮次" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "代理回合" + } + } + } + }, + "agentTurnRail.notInScrollback": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لم يعد في سجل التمرير" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Više nije u historiji pomicanja" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Ikke længere i rullehistorikken" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht mehr im Scrollback" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "No longer in scrollback" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya no está en el historial de desplazamiento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "N’est plus dans l’historique de défilement" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non più nello scrollback" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スクロールバックにありません" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "스크롤백에 더 이상 없음" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Ikke lenger i rullehistorikken" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Już poza historią przewijania" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não está mais no histórico de rolagem" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Больше нет в истории прокрутки" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ไม่อยู่ในประวัติการเลื่อนแล้ว" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Artık kaydırma geçmişinde değil" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Більше немає в історії прокручування" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已不在回滚记录中" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已不在捲動記錄中" + } + } + } + }, "cloudTree.devices.empty": { "extractionState": "manual", "localizations": { @@ -649423,382 +649069,1149 @@ } } }, - "dialog.remoteTmux.newSessionFailed.message": { + "settings.betaFeatures.clickByHint": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "فشل tmux new-session على المضيف البعيد. لم يتم إنشاء مساحة عمل." + "value": "النقر عبر التلميح" } }, - "bs": { + "de": { "stringUnit": { "state": "translated", - "value": "tmux new-session nije uspio na udaljenom hostu. Radni prostor nije kreiran." + "value": "Per Hinweis klicken" } }, - "da": { + "en": { "stringUnit": { "state": "translated", - "value": "tmux new-session mislykkedes på fjernværten. Der blev ikke oprettet noget arbejdsområde." + "value": "Click by Hint" } }, - "de": { + "es": { "stringUnit": { "state": "translated", - "value": "tmux new-session ist auf dem entfernten Host fehlgeschlagen. Es wurde kein Arbeitsbereich erstellt." + "value": "Clic por indicación" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cliquer par repère" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ヒントでクリック" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "힌트로 클릭" } }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "按提示点击" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "依提示點按" + } + } + } + }, + "remoteSession.parked.sshAuthenticationFailed": { + "extractionState": "manual", + "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "tmux new-session failed on the remote host. No workspace was created." + "value": "SSH authentication failed. Automatic reconnect paused; use Reconnect to try again." } }, - "es": { + "ja": { "stringUnit": { "state": "translated", - "value": "tmux new-session falló en el host remoto. No se creó ningún espacio de trabajo." + "value": "SSH 認証に失敗しました。自動再接続を一時停止しました。「再接続」で再試行してください。" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "SSH 身份验证失败。自动重连已暂停;请使用“重新连接”再试。" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die SSH-Authentifizierung ist fehlgeschlagen. Die automatische Wiederverbindung ist pausiert; verwenden Sie „Wieder verbinden“, um es erneut zu versuchen." } }, "fr": { "stringUnit": { "state": "translated", - "value": "tmux new-session a échoué sur l’hôte distant. Aucun espace de travail n’a été créé." + "value": "L’authentification SSH a échoué. La reconnexion automatique est en pause ; utilisez Reconnecter pour réessayer." } }, - "it": { + "ar": { "stringUnit": { "state": "translated", - "value": "tmux new-session non è riuscito sull’host remoto. Non è stata creata alcuna area di lavoro." + "value": "فشلت مصادقة SSH. تم إيقاف إعادة الاتصال التلقائي مؤقتًا؛ استخدم \"أعد الاتصال\" للمحاولة مرة أخرى." } }, - "ja": { + "es": { "stringUnit": { "state": "translated", - "value": "リモートホストで tmux new-session が失敗しました。ワークスペースは作成されませんでした。" + "value": "La autenticación SSH falló. La reconexión automática está pausada; usa Reconectar para volver a intentarlo." } }, - "km": { + "zh-Hant": { "stringUnit": { "state": "translated", - "value": "tmux new-session បានបរាជ័យនៅលើម៉ាស៊ីនពីចម្ងាយ។ មិនមានកន្លែងធ្វើការត្រូវបានបង្កើតទេ។" + "value": "SSH 驗證失敗。自動重新連線已暫停;請使用「重新連接」重試。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "원격 호스트에서 tmux new-session이 실패했습니다. 작업 공간이 생성되지 않았습니다." + "value": "SSH 인증에 실패했습니다. 자동 재연결이 일시 중지되었습니다. '재연결'을 사용하여 다시 시도하세요." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "SSH provjera autentičnosti nije uspjela. Automatsko ponovno povezivanje je pauzirano; koristite Ponovo poveži da pokušate ponovo." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "SSH-godkendelse mislykkedes. Automatisk genforbindelse er sat på pause; brug Forbind igen for at prøve igen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Autenticazione SSH non riuscita. La riconnessione automatica è in pausa; usa Riconnetti per riprovare." + } + }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ការផ្ទៀងផ្ទាត់ SSH បានបរាជ័យ។ ការភ្ជាប់ឡើងវិញដោយស្វ័យប្រវត្តិត្រូវបានផ្អាក។ ប្រើ «ភ្ជាប់ឡើងវិញ» ដើម្បីព្យាយាមម្ដងទៀត។" } }, "nb": { "stringUnit": { "state": "translated", - "value": "tmux new-session mislyktes på den eksterne verten. Det ble ikke opprettet noe arbeidsområde." + "value": "SSH-autentisering mislyktes. Automatisk tilkobling på nytt er satt på pause; bruk Koble til på nytt for å prøve igjen." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Polecenie tmux new-session nie powiodło się na hoście zdalnym. Nie utworzono przestrzeni roboczej." + "value": "Uwierzytelnianie SSH nie powiodło się. Automatyczne ponowne łączenie jest wstrzymane; użyj opcji Połącz ponownie, aby spróbować jeszcze raz." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "tmux new-session falhou no host remoto. Nenhuma área de trabalho foi criada." + "value": "Falha na autenticação SSH. A reconexão automática está pausada; use Reconectar para tentar novamente." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Команда tmux new-session завершилась с ошибкой на удалённом хосте. Рабочее пространство не создано." + "value": "Не удалось выполнить аутентификацию SSH. Автоматическое переподключение приостановлено; нажмите «Подключиться снова», чтобы повторить попытку." } }, "th": { "stringUnit": { "state": "translated", - "value": "tmux new-session ล้มเหลวบนโฮสต์ระยะไกล ไม่ได้สร้างเวิร์กสเปซ" + "value": "การตรวจสอบสิทธิ์ SSH ล้มเหลว การเชื่อมต่ออัตโนมัติถูกหยุดชั่วคราว ใช้ “เชื่อมต่อใหม่” เพื่อลองอีกครั้ง" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Uzak ana bilgisayarda tmux new-session başarısız oldu. Çalışma alanı oluşturulmadı." + "value": "SSH kimlik doğrulaması başarısız oldu. Otomatik yeniden bağlanma duraklatıldı; yeniden denemek için Yeniden Bağlan'ı kullanın." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Команда tmux new-session завершилася з помилкою на віддаленому хості. Робочу область не створено." + "value": "Не вдалося автентифікуватися через SSH. Автоматичне повторне підключення призупинено; натисніть «Підключитися знову», щоб спробувати ще раз." + } + } + } + }, + "cloud.welcome.introducing": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Introducing" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "نقدّم لك" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Presentamos" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新登場" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새로운 기능" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "远程主机上的 tmux new-session 失败。未创建工作区。" + "value": "隆重推出" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "遠端主機上的 tmux new-session 失敗。未建立工作區。" + "value": "隆重推出" } } } }, - "dialog.remoteTmux.newSessionFailed.title": { + "cloud.welcome.introducingCloud": { "extractionState": "manual", "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Introducing cmux cloud" + } + }, "ar": { "stringUnit": { "state": "translated", - "value": "تعذّر إنشاء جلسة tmux على %@" + "value": "نقدّم لك cmux cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu: cmux cloud" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Presentamos cmux cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Découvrez cmux cloud" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud 新登場" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux cloud를 소개합니다" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "隆重推出 cmux cloud" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "隆重推出 cmux cloud" + } + } + } + }, + "cloud.welcome.slide.displays.line": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch agents drive a real browser on the machine." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شاهد الوكلاء يقودون متصفحًا حقيقيًا على الجهاز." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sieh zu, wie Agenten einen echten Browser auf der Maschine steuern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mira cómo los agentes controlan un navegador real en la máquina." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Regardez les agents piloter un vrai navigateur sur la machine." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントがマシン上で実際のブラウザを操作する様子を確認できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트가 머신에서 실제 브라우저를 조작하는 모습을 확인하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "观看智能体在机器上操作真正的浏览器。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "觀看代理在機器上操作真正的瀏覽器。" + } + } + } + }, + "cloud.welcome.slide.invite.line": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add teammates by email to your team’s machines." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أضف زملاءك عبر البريد الإلكتروني إلى أجهزة فريقك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge Teammitglieder per E-Mail zu den Maschinen deines Teams hinzu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade compañeros por correo a las máquinas de tu equipo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez des coéquipiers par e-mail aux machines de votre équipe." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "メールでチームメイトをチームのマシンに追加できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이메일로 팀원을 팀 머신에 추가하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "通过电子邮件将队友添加到团队的机器。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "透過電子郵件將隊友加入團隊的機器。" + } + } + } + }, + "cloud.welcome.slide.keepsRunning.line": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close your laptop. Your agents keep working." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أغلق حاسوبك المحمول. يواصل وكلاؤك العمل." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Klapp deinen Laptop zu. Deine Agenten arbeiten weiter." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cierra tu portátil. Tus agentes siguen trabajando." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermez votre ordinateur. Vos agents continuent de travailler." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ノートパソコンを閉じても、エージェントは作業を続けます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "노트북을 닫아도 에이전트는 계속 작업합니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "合上笔记本,你的智能体会继续工作。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "闔上筆電,您的代理會繼續工作。" + } + } + } + }, + "cloud.welcome.slide.spinUp.line": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Give every agent task its own machine, ready in seconds." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "امنح كل مهمة وكيل جهازها الخاص، جاهزًا في ثوانٍ." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gib jeder Agentenaufgabe ihre eigene Maschine, in Sekunden bereit." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Da a cada tarea de agente su propia máquina, lista en segundos." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Donnez à chaque tâche d’agent sa propre machine, prête en quelques secondes." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントのタスクごとに専用マシンを数秒で用意できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트 작업마다 전용 머신을 몇 초 만에 준비하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "为每个智能体任务提供专属机器,几秒内就绪。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "為每個代理任務提供專屬機器,幾秒內就緒。" + } + } + } + }, + "cloud.welcome.slide.team.line": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Work on the same machine as your teammates, live." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "اعمل مباشرةً على الجهاز نفسه مع زملائك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeite live mit deinen Teammitgliedern auf derselben Maschine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Trabaja en vivo en la misma máquina que tus compañeros." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Travaillez en direct sur la même machine que vos coéquipiers." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "チームメイトと同じマシンでリアルタイムに作業できます。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "팀원과 같은 머신에서 실시간으로 작업하세요." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "与队友实时在同一台机器上工作。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "與隊友即時在同一台機器上工作。" + } + } + } + }, + "cloud.welcome.note.alreadyIncluded": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Already included in your plan." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "مضمَّن بالفعل في خطتك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereits in deinem Tarif enthalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya incluido en tu plan." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déjà inclus dans votre forfait." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ご利用のプランにすでに含まれています。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "이미 현재 요금제에 포함되어 있습니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "已包含在你的方案中。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "已包含在您的方案中。" + } + } + } + }, + "cloud.welcome.headline": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud machines for your agents" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أجهزة سحابية لوكلائك" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Maschinen für deine Agenten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Máquinas en la nube para tus agentes" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Des machines cloud pour vos agents" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エージェントのためのクラウドマシン" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "에이전트를 위한 클라우드 머신" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "为你的智能体准备的云端机器" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "為您的代理準備的雲端機器" + } + } + } + }, + "debug.menu.showCloudWelcomeFBadge": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Cloud Welcome (F, New Badge)…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار ترحيب Cloud (F، شارة جديد)…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Willkommen anzeigen (F, Neu-Badge)…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar bienvenida de Cloud (F, insignia Nuevo)…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’accueil Cloud (F, badge Nouveau)…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud のようこそ画面を表示(F、新バッジ)…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 환영 화면 보기 (F, 새 배지)…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示 Cloud 欢迎界面(F,新标记)…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示 Cloud 歡迎畫面(F,新標章)…" + } + } + } + }, + "debug.menu.showCloudWelcomeFOnClip": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Cloud Welcome (F, On Clip)…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار ترحيب Cloud (F، على المقطع)…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Willkommen anzeigen (F, auf Clip)…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar bienvenida de Cloud (F, sobre el clip)…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’accueil Cloud (F, sur le clip)…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud のようこそ画面を表示(F、クリップ上)…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 환영 화면 보기 (F, 클립 위)…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示 Cloud 欢迎界面(F,叠在视频上)…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示 Cloud 歡迎畫面(F,疊在影片上)…" + } + } + } + }, + "debug.menu.showCloudWelcomeHeadline": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Cloud Welcome (Headline)…" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إظهار ترحيب Cloud (العنوان)…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Willkommen anzeigen (Überschrift)…" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar bienvenida de Cloud (titular)…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’accueil Cloud (titre)…" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Cloud のようこそ画面を表示(見出し)…" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Cloud 환영 화면 보기 (헤드라인)…" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "显示 Cloud 欢迎界面(标题)…" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "顯示 Cloud 歡迎畫面(標題)…" + } + } + } + }, + "dialog.remoteTmux.newSessionFailed.message": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "فشل tmux new-session على المضيف البعيد. لم يتم إنشاء مساحة عمل." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Nije moguće kreirati tmux sesiju na %@" + "value": "tmux new-session nije uspio na udaljenom hostu. Radni prostor nije kreiran." } }, "da": { "stringUnit": { "state": "translated", - "value": "Kunne ikke oprette en tmux-session på %@" + "value": "tmux new-session mislykkedes på fjernværten. Der blev ikke oprettet noget arbejdsområde." } }, "de": { "stringUnit": { "state": "translated", - "value": "tmux-Sitzung auf %@ konnte nicht erstellt werden" + "value": "tmux new-session ist auf dem entfernten Host fehlgeschlagen. Es wurde kein Arbeitsbereich erstellt." } }, "en": { "stringUnit": { "state": "translated", - "value": "Couldn't Create a tmux Session on %@" + "value": "tmux new-session failed on the remote host. No workspace was created." } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo crear una sesión de tmux en %@" + "value": "tmux new-session falló en el host remoto. No se creó ningún espacio de trabajo." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible de créer une session tmux sur %@" + "value": "tmux new-session a échoué sur l’hôte distant. Aucun espace de travail n’a été créé." } }, "it": { "stringUnit": { "state": "translated", - "value": "Impossibile creare una sessione tmux su %@" + "value": "tmux new-session non è riuscito sull’host remoto. Non è stata creata alcuna area di lavoro." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%@ で tmux セッションを作成できませんでした" + "value": "リモートホストで tmux new-session が失敗しました。ワークスペースは作成されませんでした。" } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនអាចបង្កើត tmux session នៅលើ %@ បានទេ" + "value": "tmux new-session បានបរាជ័យនៅលើម៉ាស៊ីនពីចម្ងាយ។ មិនមានកន្លែងធ្វើការត្រូវបានបង្កើតទេ។" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@에서 tmux 세션을 만들 수 없습니다" + "value": "원격 호스트에서 tmux new-session이 실패했습니다. 작업 공간이 생성되지 않았습니다." } }, "nb": { "stringUnit": { "state": "translated", - "value": "Kunne ikke opprette en tmux-økt på %@" + "value": "tmux new-session mislyktes på den eksterne verten. Det ble ikke opprettet noe arbeidsområde." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Nie można utworzyć sesji tmux na %@" + "value": "Polecenie tmux new-session nie powiodło się na hoście zdalnym. Nie utworzono przestrzeni roboczej." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Não foi possível criar uma sessão do tmux em %@" + "value": "tmux new-session falhou no host remoto. Nenhuma área de trabalho foi criada." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Не удалось создать сеанс tmux на %@" + "value": "Команда tmux new-session завершилась с ошибкой на удалённом хосте. Рабочее пространство не создано." } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่สามารถสร้างเซสชัน tmux บน %@ ได้" + "value": "tmux new-session ล้มเหลวบนโฮสต์ระยะไกล ไม่ได้สร้างเวิร์กสเปซ" } }, "tr": { "stringUnit": { "state": "translated", - "value": "%@ üzerinde tmux oturumu oluşturulamadı" + "value": "Uzak ana bilgisayarda tmux new-session başarısız oldu. Çalışma alanı oluşturulmadı." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Не вдалося створити сеанс tmux на %@" + "value": "Команда tmux new-session завершилася з помилкою на віддаленому хості. Робочу область не створено." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法在 %@ 上创建 tmux 会话" + "value": "远程主机上的 tmux new-session 失败。未创建工作区。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法在 %@ 上建立 tmux 工作階段" + "value": "遠端主機上的 tmux new-session 失敗。未建立工作區。" } } } }, - "menu.file.newLocalWorkspace": { + "dialog.remoteTmux.newSessionFailed.title": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "مساحة عمل محلية جديدة" + "value": "تعذّر إنشاء جلسة tmux على %@" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Novi lokalni radni prostor" + "value": "Nije moguće kreirati tmux sesiju na %@" } }, "da": { "stringUnit": { "state": "translated", - "value": "Nyt lokalt arbejdsområde" + "value": "Kunne ikke oprette en tmux-session på %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Neuer lokaler Arbeitsbereich" + "value": "tmux-Sitzung auf %@ konnte nicht erstellt werden" } }, "en": { "stringUnit": { "state": "translated", - "value": "New Local Workspace" + "value": "Couldn't Create a tmux Session on %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "Nuevo espacio de trabajo local" + "value": "No se pudo crear una sesión de tmux en %@" } }, "fr": { "stringUnit": { "state": "translated", - "value": "Nouvel espace de travail local" + "value": "Impossible de créer une session tmux sur %@" } }, "it": { "stringUnit": { "state": "translated", - "value": "Nuova area di lavoro locale" + "value": "Impossibile creare una sessione tmux su %@" } }, "ja": { "stringUnit": { "state": "translated", - "value": "新規ローカルワークスペース" + "value": "%@ で tmux セッションを作成できませんでした" } }, "km": { "stringUnit": { "state": "translated", - "value": "កន្លែងធ្វើការមូលដ្ឋានថ្មី" + "value": "មិនអាចបង្កើត tmux session នៅលើ %@ បានទេ" } }, "ko": { "stringUnit": { "state": "translated", - "value": "새 로컬 작업 공간" + "value": "%@에서 tmux 세션을 만들 수 없습니다" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Nytt lokalt arbeidsområde" + "value": "Kunne ikke opprette en tmux-økt på %@" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Nowa lokalna przestrzeń robocza" + "value": "Nie można utworzyć sesji tmux na %@" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Nova Área de Trabalho Local" + "value": "Não foi possível criar uma sessão do tmux em %@" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Новое локальное рабочее пространство" + "value": "Не удалось создать сеанс tmux на %@" } }, "th": { "stringUnit": { "state": "translated", - "value": "เวิร์กสเปซในเครื่องใหม่" + "value": "ไม่สามารถสร้างเซสชัน tmux บน %@ ได้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Yeni Yerel Çalışma Alanı" + "value": "%@ üzerinde tmux oturumu oluşturulamadı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Нова локальна робоча область" + "value": "Не вдалося створити сеанс tmux на %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "新建本地工作区" + "value": "无法在 %@ 上创建 tmux 会话" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "新增本機工作區" + "value": "無法在 %@ 上建立 tmux 工作階段" } } } }, - "shortcut.newLocalWorkspace.label": { + "menu.file.newLocalWorkspace": { "extractionState": "manual", "localizations": { "ar": { @@ -649923,620 +650336,502 @@ } } }, - "dialog.remoteTmux.newSessionAttachFailed.left.message": { + "shortcut.newLocalWorkspace.label": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "تم إنشاء الجلسة، لكن تعذّر على cmux الاتصال بها. ما زالت تعمل على المضيف." + "value": "مساحة عمل محلية جديدة" } }, "bs": { "stringUnit": { "state": "translated", - "value": "Sesija je kreirana, ali se cmux nije mogao povezati s njom. Još uvijek radi na hostu." + "value": "Novi lokalni radni prostor" } }, "da": { "stringUnit": { "state": "translated", - "value": "Sessionen blev oprettet, men cmux kunne ikke tilslutte sig den. Den kører stadig på værten." + "value": "Nyt lokalt arbejdsområde" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Sitzung wurde erstellt, aber cmux konnte sich nicht mit ihr verbinden. Sie läuft weiterhin auf dem Host." + "value": "Neuer lokaler Arbeitsbereich" } }, "en": { "stringUnit": { "state": "translated", - "value": "The session was created, but cmux couldn't attach to it. It is still running on the host." + "value": "New Local Workspace" } }, "es": { "stringUnit": { "state": "translated", - "value": "La sesión se creó, pero cmux no pudo conectarse a ella. Sigue ejecutándose en el host." + "value": "Nuevo espacio de trabajo local" } }, "fr": { "stringUnit": { "state": "translated", - "value": "La session a été créée, mais cmux n’a pas pu s’y attacher. Elle est toujours en cours d’exécution sur l’hôte." + "value": "Nouvel espace de travail local" } }, "it": { "stringUnit": { "state": "translated", - "value": "La sessione è stata creata, ma cmux non è riuscito a collegarsi. È ancora in esecuzione sull’host." + "value": "Nuova area di lavoro locale" } }, "ja": { "stringUnit": { "state": "translated", - "value": "セッションは作成されましたが、cmux が接続できませんでした。ホスト上でまだ実行中です。" + "value": "新規ローカルワークスペース" } }, "km": { "stringUnit": { "state": "translated", - "value": "session ត្រូវបានបង្កើត ប៉ុន្តែ cmux មិនអាចភ្ជាប់ទៅវាបានទេ។ វានៅតែដំណើរការនៅលើម៉ាស៊ីន។" + "value": "កន្លែងធ្វើការមូលដ្ឋានថ្មី" } }, "ko": { "stringUnit": { "state": "translated", - "value": "세션이 생성되었지만 cmux가 연결할 수 없습니다. 호스트에서 계속 실행 중입니다." + "value": "새 로컬 작업 공간" } }, "nb": { "stringUnit": { "state": "translated", - "value": "Økten ble opprettet, men cmux kunne ikke koble seg til den. Den kjører fortsatt på verten." + "value": "Nytt lokalt arbeidsområde" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Sesja została utworzona, ale cmux nie mógł się do niej podłączyć. Nadal działa na hoście." + "value": "Nowa lokalna przestrzeń robocza" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A sessão foi criada, mas o cmux não conseguiu se conectar a ela. Ela continua em execução no host." + "value": "Nova Área de Trabalho Local" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Сеанс был создан, но cmux не смог к нему подключиться. Он по-прежнему работает на хосте." + "value": "Новое локальное рабочее пространство" } }, "th": { "stringUnit": { "state": "translated", - "value": "สร้างเซสชันแล้ว แต่ cmux เชื่อมต่อไม่ได้ เซสชันยังทำงานอยู่บนโฮสต์" + "value": "เวิร์กสเปซในเครื่องใหม่" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Oturum oluşturuldu, ancak cmux ona bağlanamadı. Ana bilgisayarda çalışmaya devam ediyor." + "value": "Yeni Yerel Çalışma Alanı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Сеанс було створено, але cmux не зміг до нього під’єднатися. Він досі працює на хості." + "value": "Нова локальна робоча область" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "会话已创建,但 cmux 无法连接到它。它仍在主机上运行。" + "value": "新建本地工作区" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "工作階段已建立,但 cmux 無法連接。它仍在主機上執行。" + "value": "新增本機工作區" } } } }, - "dialog.remoteTmux.newSessionAttachFailed.removed.message": { + "dialog.remoteTmux.newSessionAttachFailed.left.message": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "تم إنشاء الجلسة، لكن تعذّر على cmux الاتصال بها، فتمت إزالتها من المضيف." + "value": "تم إنشاء الجلسة، لكن تعذّر على cmux الاتصال بها. ما زالت تعمل على المضيف." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Sesija je kreirana, ali se cmux nije mogao povezati s njom, pa je uklonjena s hosta." + "value": "Sesija je kreirana, ali se cmux nije mogao povezati s njom. Još uvijek radi na hostu." } }, "da": { "stringUnit": { "state": "translated", - "value": "Sessionen blev oprettet, men cmux kunne ikke tilslutte sig den, så den blev fjernet fra værten." + "value": "Sessionen blev oprettet, men cmux kunne ikke tilslutte sig den. Den kører stadig på værten." } }, "de": { "stringUnit": { "state": "translated", - "value": "Die Sitzung wurde erstellt, aber cmux konnte sich nicht mit ihr verbinden. Sie wurde daher vom Host entfernt." + "value": "Die Sitzung wurde erstellt, aber cmux konnte sich nicht mit ihr verbinden. Sie läuft weiterhin auf dem Host." } }, "en": { "stringUnit": { "state": "translated", - "value": "The session was created, but cmux couldn't attach to it, so it was removed from the host." + "value": "The session was created, but cmux couldn't attach to it. It is still running on the host." } }, "es": { "stringUnit": { "state": "translated", - "value": "La sesión se creó, pero cmux no pudo conectarse a ella, así que se eliminó del host." + "value": "La sesión se creó, pero cmux no pudo conectarse a ella. Sigue ejecutándose en el host." } }, "fr": { "stringUnit": { "state": "translated", - "value": "La session a été créée, mais cmux n’a pas pu s’y attacher. Elle a donc été supprimée de l’hôte." + "value": "La session a été créée, mais cmux n’a pas pu s’y attacher. Elle est toujours en cours d’exécution sur l’hôte." } }, "it": { "stringUnit": { "state": "translated", - "value": "La sessione è stata creata, ma cmux non è riuscito a collegarsi, quindi è stata rimossa dall’host." + "value": "La sessione è stata creata, ma cmux non è riuscito a collegarsi. È ancora in esecuzione sull’host." } }, "ja": { "stringUnit": { "state": "translated", - "value": "セッションは作成されましたが、cmux が接続できなかったため、ホストから削除されました。" + "value": "セッションは作成されましたが、cmux が接続できませんでした。ホスト上でまだ実行中です。" } }, "km": { "stringUnit": { "state": "translated", - "value": "session ត្រូវបានបង្កើត ប៉ុន្តែ cmux មិនអាចភ្ជាប់ទៅវាបានទេ ដូច្នេះវាត្រូវបានលុបចេញពីម៉ាស៊ីន។" + "value": "session ត្រូវបានបង្កើត ប៉ុន្តែ cmux មិនអាចភ្ជាប់ទៅវាបានទេ។ វានៅតែដំណើរការនៅលើម៉ាស៊ីន។" } }, "ko": { "stringUnit": { "state": "translated", - "value": "세션이 생성되었지만 cmux가 연결할 수 없어 호스트에서 제거했습니다." + "value": "세션이 생성되었지만 cmux가 연결할 수 없습니다. 호스트에서 계속 실행 중입니다." } }, "nb": { "stringUnit": { "state": "translated", - "value": "Økten ble opprettet, men cmux kunne ikke koble seg til den, så den ble fjernet fra verten." + "value": "Økten ble opprettet, men cmux kunne ikke koble seg til den. Den kjører fortsatt på verten." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Sesja została utworzona, ale cmux nie mógł się do niej podłączyć, więc usunięto ją z hosta." + "value": "Sesja została utworzona, ale cmux nie mógł się do niej podłączyć. Nadal działa na hoście." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "A sessão foi criada, mas o cmux não conseguiu se conectar a ela, então ela foi removida do host." + "value": "A sessão foi criada, mas o cmux não conseguiu se conectar a ela. Ela continua em execução no host." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Сеанс был создан, но cmux не смог к нему подключиться, поэтому сеанс удалён с хоста." + "value": "Сеанс был создан, но cmux не смог к нему подключиться. Он по-прежнему работает на хосте." } }, "th": { "stringUnit": { "state": "translated", - "value": "สร้างเซสชันแล้ว แต่ cmux เชื่อมต่อไม่ได้ จึงลบเซสชันออกจากโฮสต์แล้ว" + "value": "สร้างเซสชันแล้ว แต่ cmux เชื่อมต่อไม่ได้ เซสชันยังทำงานอยู่บนโฮสต์" } }, "tr": { "stringUnit": { "state": "translated", - "value": "Oturum oluşturuldu, ancak cmux ona bağlanamadı; bu nedenle ana bilgisayardan kaldırıldı." + "value": "Oturum oluşturuldu, ancak cmux ona bağlanamadı. Ana bilgisayarda çalışmaya devam ediyor." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Сеанс було створено, але cmux не зміг до нього під’єднатися, тому сеанс видалено з хоста." + "value": "Сеанс було створено, але cmux не зміг до нього під’єднатися. Він досі працює на хості." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "会话已创建,但 cmux 无法连接到它,因此已将其从主机上移除。" + "value": "会话已创建,但 cmux 无法连接到它。它仍在主机上运行。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "工作階段已建立,但 cmux 無法連接,因此已從主機上移除。" + "value": "工作階段已建立,但 cmux 無法連接。它仍在主機上執行。" } } } }, - "dialog.remoteTmux.newSessionAttachFailed.title": { + "dialog.remoteTmux.newSessionAttachFailed.removed.message": { "extractionState": "manual", "localizations": { "ar": { "stringUnit": { "state": "translated", - "value": "تعذّر فتح جلسة tmux الجديدة على %@" + "value": "تم إنشاء الجلسة، لكن تعذّر على cmux الاتصال بها، فتمت إزالتها من المضيف." } }, "bs": { "stringUnit": { "state": "translated", - "value": "Nije moguće otvoriti novu tmux sesiju na %@" + "value": "Sesija je kreirana, ali se cmux nije mogao povezati s njom, pa je uklonjena s hosta." } }, "da": { "stringUnit": { "state": "translated", - "value": "Kunne ikke åbne den nye tmux-session på %@" + "value": "Sessionen blev oprettet, men cmux kunne ikke tilslutte sig den, så den blev fjernet fra værten." } }, "de": { "stringUnit": { "state": "translated", - "value": "Neue tmux-Sitzung auf %@ konnte nicht geöffnet werden" + "value": "Die Sitzung wurde erstellt, aber cmux konnte sich nicht mit ihr verbinden. Sie wurde daher vom Host entfernt." } }, "en": { "stringUnit": { "state": "translated", - "value": "Couldn't Open the New tmux Session on %@" + "value": "The session was created, but cmux couldn't attach to it, so it was removed from the host." } }, "es": { "stringUnit": { "state": "translated", - "value": "No se pudo abrir la nueva sesión de tmux en %@" + "value": "La sesión se creó, pero cmux no pudo conectarse a ella, así que se eliminó del host." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Impossible d’ouvrir la nouvelle session tmux sur %@" + "value": "La session a été créée, mais cmux n’a pas pu s’y attacher. Elle a donc été supprimée de l’hôte." } }, "it": { "stringUnit": { "state": "translated", - "value": "Impossibile aprire la nuova sessione tmux su %@" + "value": "La sessione è stata creata, ma cmux non è riuscito a collegarsi, quindi è stata rimossa dall’host." } }, "ja": { "stringUnit": { "state": "translated", - "value": "%@ の新しい tmux セッションを開けませんでした" + "value": "セッションは作成されましたが、cmux が接続できなかったため、ホストから削除されました。" } }, "km": { "stringUnit": { "state": "translated", - "value": "មិនអាចបើក tmux session ថ្មីនៅលើ %@ បានទេ" + "value": "session ត្រូវបានបង្កើត ប៉ុន្តែ cmux មិនអាចភ្ជាប់ទៅវាបានទេ ដូច្នេះវាត្រូវបានលុបចេញពីម៉ាស៊ីន។" } }, "ko": { "stringUnit": { "state": "translated", - "value": "%@에서 새 tmux 세션을 열 수 없습니다" + "value": "세션이 생성되었지만 cmux가 연결할 수 없어 호스트에서 제거했습니다." } }, "nb": { "stringUnit": { "state": "translated", - "value": "Kunne ikke åpne den nye tmux-økten på %@" + "value": "Økten ble opprettet, men cmux kunne ikke koble seg til den, så den ble fjernet fra verten." } }, "pl": { "stringUnit": { "state": "translated", - "value": "Nie można otworzyć nowej sesji tmux na %@" + "value": "Sesja została utworzona, ale cmux nie mógł się do niej podłączyć, więc usunięto ją z hosta." } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Não foi possível abrir a nova sessão do tmux em %@" + "value": "A sessão foi criada, mas o cmux não conseguiu se conectar a ela, então ela foi removida do host." } }, "ru": { "stringUnit": { "state": "translated", - "value": "Не удалось открыть новый сеанс tmux на %@" + "value": "Сеанс был создан, но cmux не смог к нему подключиться, поэтому сеанс удалён с хоста." } }, "th": { "stringUnit": { "state": "translated", - "value": "ไม่สามารถเปิดเซสชัน tmux ใหม่บน %@ ได้" + "value": "สร้างเซสชันแล้ว แต่ cmux เชื่อมต่อไม่ได้ จึงลบเซสชันออกจากโฮสต์แล้ว" } }, "tr": { "stringUnit": { "state": "translated", - "value": "%@ üzerindeki yeni tmux oturumu açılamadı" + "value": "Oturum oluşturuldu, ancak cmux ona bağlanamadı; bu nedenle ana bilgisayardan kaldırıldı." } }, "uk": { "stringUnit": { "state": "translated", - "value": "Не вдалося відкрити новий сеанс tmux на %@" + "value": "Сеанс було створено, але cmux не зміг до нього під’єднатися, тому сеанс видалено з хоста." } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "无法打开 %@ 上的新 tmux 会话" + "value": "会话已创建,但 cmux 无法连接到它,因此已将其从主机上移除。" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "無法開啟 %@ 上的新 tmux 工作階段" + "value": "工作階段已建立,但 cmux 無法連接,因此已從主機上移除。" } } } }, - "remoteSession.parked.sshAuthenticationFailed": { + "dialog.remoteTmux.newSessionAttachFailed.title": { "extractionState": "manual", "localizations": { - "en": { + "ar": { "stringUnit": { "state": "translated", - "value": "SSH authentication failed. Automatic reconnect paused; use Reconnect to try again." + "value": "تعذّر فتح جلسة tmux الجديدة على %@" } }, - "ja": { + "bs": { "stringUnit": { "state": "translated", - "value": "SSH 認証に失敗しました。自動再接続を一時停止しました。「再接続」で再試行してください。" + "value": "Nije moguće otvoriti novu tmux sesiju na %@" } }, - "zh-Hans": { + "da": { "stringUnit": { "state": "translated", - "value": "SSH 身份验证失败。自动重连已暂停;请使用“重新连接”再试。" + "value": "Kunne ikke åbne den nye tmux-session på %@" } }, "de": { "stringUnit": { "state": "translated", - "value": "Die SSH-Authentifizierung ist fehlgeschlagen. Die automatische Wiederverbindung ist pausiert; verwenden Sie „Wieder verbinden“, um es erneut zu versuchen." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "L’authentification SSH a échoué. La reconnexion automatique est en pause ; utilisez Reconnecter pour réessayer." + "value": "Neue tmux-Sitzung auf %@ konnte nicht geöffnet werden" } }, - "ar": { + "en": { "stringUnit": { "state": "translated", - "value": "فشلت مصادقة SSH. تم إيقاف إعادة الاتصال التلقائي مؤقتًا؛ استخدم \"أعد الاتصال\" للمحاولة مرة أخرى." + "value": "Couldn't Open the New tmux Session on %@" } }, "es": { "stringUnit": { "state": "translated", - "value": "La autenticación SSH falló. La reconexión automática está pausada; usa Reconectar para volver a intentarlo." - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "SSH 驗證失敗。自動重新連線已暫停;請使用「重新連接」重試。" + "value": "No se pudo abrir la nueva sesión de tmux en %@" } }, - "ko": { + "fr": { "stringUnit": { "state": "translated", - "value": "SSH 인증에 실패했습니다. 자동 재연결이 일시 중지되었습니다. '재연결'을 사용하여 다시 시도하세요." + "value": "Impossible d’ouvrir la nouvelle session tmux sur %@" } }, - "bs": { + "it": { "stringUnit": { "state": "translated", - "value": "SSH provjera autentičnosti nije uspjela. Automatsko ponovno povezivanje je pauzirano; koristite Ponovo poveži da pokušate ponovo." + "value": "Impossibile aprire la nuova sessione tmux su %@" } }, - "da": { + "ja": { "stringUnit": { "state": "translated", - "value": "SSH-godkendelse mislykkedes. Automatisk genforbindelse er sat på pause; brug Forbind igen for at prøve igen." + "value": "%@ の新しい tmux セッションを開けませんでした" } }, - "it": { + "km": { "stringUnit": { "state": "translated", - "value": "Autenticazione SSH non riuscita. La riconnessione automatica è in pausa; usa Riconnetti per riprovare." + "value": "មិនអាចបើក tmux session ថ្មីនៅលើ %@ បានទេ" } }, - "km": { + "ko": { "stringUnit": { "state": "translated", - "value": "ការផ្ទៀងផ្ទាត់ SSH បានបរាជ័យ។ ការភ្ជាប់ឡើងវិញដោយស្វ័យប្រវត្តិត្រូវបានផ្អាក។ ប្រើ «ភ្ជាប់ឡើងវិញ» ដើម្បីព្យាយាមម្ដងទៀត។" + "value": "%@에서 새 tmux 세션을 열 수 없습니다" } }, "nb": { "stringUnit": { "state": "translated", - "value": "SSH-autentisering mislyktes. Automatisk tilkobling på nytt er satt på pause; bruk Koble til på nytt for å prøve igjen." + "value": "Kunne ikke åpne den nye tmux-økten på %@" } }, "pl": { "stringUnit": { "state": "translated", - "value": "Uwierzytelnianie SSH nie powiodło się. Automatyczne ponowne łączenie jest wstrzymane; użyj opcji Połącz ponownie, aby spróbować jeszcze raz." + "value": "Nie można otworzyć nowej sesji tmux na %@" } }, "pt-BR": { "stringUnit": { "state": "translated", - "value": "Falha na autenticação SSH. A reconexão automática está pausada; use Reconectar para tentar novamente." + "value": "Não foi possível abrir a nova sessão do tmux em %@" } }, "ru": { "stringUnit": { "state": "translated", - "value": "Не удалось выполнить аутентификацию SSH. Автоматическое переподключение приостановлено; нажмите «Подключиться снова», чтобы повторить попытку." + "value": "Не удалось открыть новый сеанс tmux на %@" } }, "th": { "stringUnit": { "state": "translated", - "value": "การตรวจสอบสิทธิ์ SSH ล้มเหลว การเชื่อมต่ออัตโนมัติถูกหยุดชั่วคราว ใช้ “เชื่อมต่อใหม่” เพื่อลองอีกครั้ง" + "value": "ไม่สามารถเปิดเซสชัน tmux ใหม่บน %@ ได้" } }, "tr": { "stringUnit": { "state": "translated", - "value": "SSH kimlik doğrulaması başarısız oldu. Otomatik yeniden bağlanma duraklatıldı; yeniden denemek için Yeniden Bağlan'ı kullanın." + "value": "%@ üzerindeki yeni tmux oturumu açılamadı" } }, "uk": { "stringUnit": { "state": "translated", - "value": "Не вдалося автентифікуватися через SSH. Автоматичне повторне підключення призупинено; натисніть «Підключитися знову», щоб спробувати ще раз." - } - } - } - }, - "ssh.tui.browserForward.ended": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "The SSH port forward ended before it was ready. Reload to reconnect." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "انتهت إعادة توجيه منفذ SSH قبل أن تصبح جاهزة. أعد التحميل لإعادة الاتصال." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die SSH-Portweiterleitung wurde beendet, bevor sie bereit war. Neu laden, um die Verbindung wiederherzustellen." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "El reenvío de puerto SSH terminó antes de estar listo. Recarga para volver a conectar." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "La redirection de port SSH s’est arrêtée avant d’être prête. Rechargez pour vous reconnecter." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "SSH ポート転送が準備完了前に終了しました。再読み込みして再接続してください。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "SSH 포트 포워딩이 준비되기 전에 종료되었습니다. 다시 연결하려면 새로고침하세요." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "SSH 端口转发在就绪前已结束。请重新加载以重新连接。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "SSH 連接埠轉送在就緒前已結束。請重新載入以重新連線。" - } - } - } - }, - "ssh.tui.browserForward.httpsUnsupported": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "HTTPS services on an SSH host can't open in the browser yet. Use http:// instead." - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لا يمكن فتح خدمات HTTPS على مضيف SSH في المتصفح بعد. استخدم http:// بدلاً من ذلك." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "HTTPS-Dienste auf einem SSH-Host können noch nicht im Browser geöffnet werden. Verwende stattdessen http://." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Los servicios HTTPS de un host SSH aún no se pueden abrir en el navegador. Usa http:// en su lugar." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Les services HTTPS d’un hôte SSH ne peuvent pas encore s’ouvrir dans le navigateur. Utilisez http:// à la place." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "SSH ホスト上の HTTPS サービスはまだブラウザで開けません。代わりに http:// を使用してください。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "SSH 호스트의 HTTPS 서비스는 아직 브라우저에서 열 수 없습니다. 대신 http://를 사용하세요." + "value": "Не вдалося відкрити новий сеанс tmux на %@" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "SSH 主机上的 HTTPS 服务暂时无法在浏览器中打开。请改用 http://。" + "value": "无法打开 %@ 上的新 tmux 会话" } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "SSH 主機上的 HTTPS 服務暫時無法在瀏覽器中開啟。請改用 http://。" + "value": "無法開啟 %@ 上的新 tmux 工作階段" } } } diff --git a/Sources/Cloud/CloudWelcomeMedia.swift b/Sources/Cloud/CloudWelcomeMedia.swift index 96ee805f7db6..fe761fad35e0 100644 --- a/Sources/Cloud/CloudWelcomeMedia.swift +++ b/Sources/Cloud/CloudWelcomeMedia.swift @@ -14,23 +14,15 @@ struct CloudWelcomeSlide: Identifiable, Equatable { let title: String let caption: String + /// Five features, ending on the one only Cloud can do. static let all: [CloudWelcomeSlide] = [ CloudWelcomeSlide( id: "spin-up", symbol: "bolt", title: String(localized: "cloud.welcome.slide.spinUp.title", defaultValue: "Create machines in seconds"), caption: String( - localized: "cloud.welcome.slide.spinUp.caption", - defaultValue: "Each one opens as a regular cmux workspace with a terminal ready." - ) - ), - CloudWelcomeSlide( - id: "keeps-running", - symbol: "terminal", - title: String(localized: "cloud.welcome.slide.keepsRunning.title", defaultValue: "Runs while you’re away"), - caption: String( - localized: "cloud.welcome.slide.keepsRunning.caption", - defaultValue: "Agents and terminals keep running after you close your laptop. Use the cmux CLI to connect local agents to your Cloud machines." + localized: "cloud.welcome.slide.spinUp.line", + defaultValue: "Give every agent task its own machine, ready in seconds." ) ), CloudWelcomeSlide( @@ -38,8 +30,8 @@ struct CloudWelcomeSlide: Identifiable, Equatable { symbol: "display", title: String(localized: "cloud.welcome.slide.displays.title", defaultValue: "See the machine’s desktop"), caption: String( - localized: "cloud.welcome.slide.displays.caption", - defaultValue: "Open a display next to your terminal and watch apps and browsers run." + localized: "cloud.welcome.slide.displays.line", + defaultValue: "Watch agents drive a real browser on the machine." ) ), CloudWelcomeSlide( @@ -47,8 +39,8 @@ struct CloudWelcomeSlide: Identifiable, Equatable { symbol: "person.badge.plus", title: String(localized: "cloud.welcome.slide.invite.title", defaultValue: "Invite your team"), caption: String( - localized: "cloud.welcome.slide.invite.caption", - defaultValue: "Add teammates by email so they can use your team’s Cloud machines." + localized: "cloud.welcome.slide.invite.line", + defaultValue: "Add teammates by email to your team’s machines." ) ), CloudWelcomeSlide( @@ -56,8 +48,17 @@ struct CloudWelcomeSlide: Identifiable, Equatable { symbol: "person.2", title: String(localized: "cloud.welcome.slide.team.title", defaultValue: "Work together on one machine"), caption: String( - localized: "cloud.welcome.slide.team.caption", - defaultValue: "Invite teammates to the same machine and collaborate in a shared workspace." + localized: "cloud.welcome.slide.team.line", + defaultValue: "Work on the same machine as your teammates, live." + ) + ), + CloudWelcomeSlide( + id: "keeps-running", + symbol: "moon", + title: String(localized: "cloud.welcome.slide.keepsRunning.title", defaultValue: "Runs while you’re away"), + caption: String( + localized: "cloud.welcome.slide.keepsRunning.line", + defaultValue: "Close your laptop. Your agents keep working." ) ), ] @@ -82,6 +83,19 @@ struct CloudWelcomeMediaCarousel: View { /// List variant: a bar for the current feature and a dot for each other one, /// each centered on its text, in place of a bar per feature. var listUsesDots = false + /// The headline shown in the poster's title band. + var posterHeadline: String? = CloudWelcomeView.headline + /// The user's New Cloud Machine shortcut, shown on the Create row. + var newMachineShortcut: String? = "⇧⌘Y" + /// The title sits in a dark band under the clip; false runs the clip full + /// bleed and puts the title under it, above the features. + var posterShowsTitle = true + /// The app icon beside the title in the band. + var posterShowsIcon = true + /// A "New" badge after the title (title under the clip only). + var showsNewBadge = false + /// The card's width; the clip fills it at 16:10. + var cardWidth: CGFloat = CloudWelcomeMediaCarousel.posterWidth /// How long a slide without a clip (or with an unreadable one) stays up. private static let fallbackDuration: Double = 4 @@ -105,22 +119,30 @@ struct CloudWelcomeMediaCarousel: View { var body: some View { Group { if showsFeatureList { - HStack(alignment: .center, spacing: 18) { + VStack(alignment: .leading, spacing: 0) { + poster + if !posterShowsTitle { + bodyTitle + .padding(.horizontal, 24) + .padding(.top, 18) + } featureList .frame(maxWidth: .infinity, alignment: .leading) - media + .padding(.horizontal, 24) + .padding(.top, 18) } - .padding(.leading, 28) - .padding(.trailing, 28) } else { stacked } } .task { await observePlayback() } .task(id: currentSlide.id) { await loadDuration() } - .task(id: AdvanceKey(slide: currentSlide.id, isPaused: playback.isPaused, duration: duration, movieFailed: movieFailed)) { + .task(id: AdvanceKey(slide: currentSlide.id, revision: playback.revision, isPaused: playback.isPaused, duration: duration, movieFailed: movieFailed)) { await advanceWhenDone() } + .onChange(of: reduceMotion, initial: true) { _, reduced in + playback.setReduceMotion(reduced) + } } /// The clip, the pager under it, and the slide's title and line. @@ -149,7 +171,123 @@ struct CloudWelcomeMediaCarousel: View { /// Beside the list the clip takes the room the titles don't need, so it /// reads larger (same 16:10, so recorded clips fit either layout). private var mediaFrame: CGSize { - showsFeatureList ? CGSize(width: 390, height: 244) : mediaSize + showsFeatureList ? CGSize(width: cardWidth, height: cardWidth / 1.6) : mediaSize + } + + // MARK: Poster + + static let posterWidth: CGFloat = 460 + /// The dark band under the clip that holds the app icon and the title. + private static let posterBandHeight: CGFloat = 64 + /// How much of the clip's bottom edge fades into the band. + private static let posterFeather: CGFloat = 44 + + /// The clip edge to edge, its bottom fading into a dark band with the app + /// icon and "Introducing cmux cloud", and a hairline that fills with the clip. + /// Always dark, like the clips, whatever the window's appearance. + private var poster: some View { + VStack(spacing: 0) { + if posterShowsTitle { + media + .mask( + LinearGradient( + stops: [ + .init(color: .black, location: 0), + .init(color: .black, location: 1 - Self.posterFeather / mediaFrame.height), + .init(color: .clear, location: 1), + ], + startPoint: .top, + endPoint: .bottom + ) + ) + Color.clear.frame(height: Self.posterBandHeight) + } else { + media + } + } + .background(Color(white: 0.05)) + .overlay(alignment: .bottomLeading) { + if posterShowsTitle { + posterTitle + .padding(.horizontal, 24) + .padding(.bottom, 16) + } + } + .overlay(alignment: .bottom) { + Rectangle() + .fill(Color.white.opacity(0.08)) + .frame(height: 1.5) + .overlay { + TimelineView(.animation(paused: playback.isPaused || reduceMotion || !autoplays)) { context in + RailFill(fraction: progress(at: context.date), vertical: false) + } + } + } + .environment(\.colorScheme, .dark) + } + + private var posterTitle: some View { + HStack(spacing: 11) { + if posterShowsIcon { + // The asset carries the icon grid's margin; 44 pt draws a ~38 pt tile. + // CloudWelcomeIcon is the dark icon without its edge bevel, which reads + // as a bright ring this small on the dark band. + Image(nsImage: NSImage(named: "CloudWelcomeIcon") ?? NSApp.applicationIconImage) + .resizable() + .interpolation(.high) + .frame(width: 44, height: 44) + .padding(EdgeInsets(top: -3, leading: -5, bottom: -3, trailing: -3)) + .accessibilityHidden(true) + } + VStack(alignment: .leading, spacing: 1) { + Text(introducingText) + .cmuxFont(size: 12) + .foregroundStyle(.white.opacity(0.55)) + Text(titleText) + .cmuxFont(size: posterShowsIcon ? 20 : 22, weight: .semibold) + .tracking(-0.3) + .foregroundStyle(.white) + } + } + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isHeader) + } + + /// The title under a full-bleed clip, above the features, with an optional + /// "New" badge. + private var bodyTitle: some View { + VStack(alignment: .leading, spacing: 2) { + Text(introducingText) + .cmuxFont(size: 12.5) + .foregroundStyle(.secondary) + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(titleText) + .cmuxFont(size: 19, weight: .semibold) + .tracking(-0.2) + if showsNewBadge { + Text(String(localized: "cloud.welcome.newBadge", defaultValue: "New")) + .cmuxFont(size: 10.5, weight: .semibold) + .foregroundStyle(Color.accentColor) + .padding(.horizontal, 7) + .frame(height: 18) + .background(Capsule(style: .continuous).fill(Color.accentColor.opacity(0.16))) + .alignmentGuide(.firstTextBaseline) { $0[.firstTextBaseline] + 2 } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityElement(children: .combine) + .accessibilityAddTraits(.isHeader) + } + + private var introducingText: String { + posterHeadline == nil + ? String(localized: "cloud.welcome.introducing", defaultValue: "Introducing") + : String(localized: "cloud.welcome.introducingCloud", defaultValue: "Introducing cmux cloud") + } + + private var titleText: String { + posterHeadline ?? String(localized: "cloud.welcome.title.eyebrow", defaultValue: "cmux cloud") } // MARK: Feature list @@ -172,38 +310,48 @@ struct CloudWelcomeMediaCarousel: View { } } - /// A feature: a segment of the rail and its title; the selected one lights - /// its segment and opens to show its line, the rest stay quiet until hovered. + /// A feature as text only: the playing one bright with its line open, the + /// rest dimmed until hovered or their turn. private func featureRow(_ slide: CloudWelcomeSlide, isCurrent: Bool, action: @escaping () -> Void) -> some View { let isHovered = hoveredRow == slide.id + let ink = Color.primary.opacity(isCurrent ? 1 : (isHovered ? 0.62 : 0.42)) return Button(action: action) { - HStack(alignment: listUsesDots ? .center : .top, spacing: 12) { - if listUsesDots { - dotMarker(isCurrent: isCurrent, isHovered: isHovered) - } else { - // Inset top and bottom so each feature has its own bar, with a - // clear gap to the next one. - railSegment(isCurrent: isCurrent) - .frame(width: 2.5) - .padding(.vertical, 4) - } + HStack(alignment: .firstTextBaseline, spacing: 12) { + Image(systemName: slide.symbol) + .cmuxFont(size: 13) + .foregroundStyle(ink) + .frame(width: 18) + .accessibilityHidden(true) VStack(alignment: .leading, spacing: 3) { Text(slide.title) - .cmuxFont(size: 13, weight: .medium) - .foregroundStyle(isCurrent || isHovered ? Color.primary : Color.secondary) + .cmuxFont(size: 13.5, weight: .medium) + .foregroundStyle(ink) .fixedSize(horizontal: false, vertical: true) if isCurrent { + // One line, so the card keeps its height in every language. Text(slide.caption) - .cmuxFont(size: 11.5) + .cmuxFont(size: 12.5) .foregroundStyle(.secondary) - .lineSpacing(1) - .fixedSize(horizontal: false, vertical: true) + .lineLimit(1) + .minimumScaleFactor(0.8) .transition(Self.captionTransition) } } - .padding(.vertical, 7) Spacer(minLength: 0) } + // Beside the title only, so the line under it keeps the full width. + .overlay(alignment: .topTrailing) { + if slide.id == "spin-up", let newMachineShortcut { + Text(newMachineShortcut) + .cmuxFont(size: 11.5) + .tracking(0.6) + .foregroundStyle(Color.primary.opacity(isCurrent ? 0.55 : 0.42)) + .padding(.top, 1) + .accessibilityHidden(true) + } + } + .padding(.vertical, 7) + .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } .buttonStyle(.plain) @@ -220,7 +368,7 @@ struct CloudWelcomeMediaCarousel: View { // MARK: Media private var media: some View { - let shape = RoundedRectangle(cornerRadius: 10, style: .continuous) + let shape = RoundedRectangle(cornerRadius: showsFeatureList ? 0 : 10, style: .continuous) return ZStack { slideMedia(currentSlide) .id(currentSlide.id) @@ -229,9 +377,12 @@ struct CloudWelcomeMediaCarousel: View { .frame(width: mediaFrame.width, height: mediaFrame.height) .background(shape.fill(Color.black.opacity(0.35))) .contentShape(shape) - .onTapGesture { - if autoplays { setPaused(!playback.isPaused) } - } + // The launch card has no pause, so its clip doesn't take the click + // and the window still drags by it. + .gesture( + TapGesture().onEnded { if autoplays { setPaused(!playback.isPaused) } }, + including: showsFeatureList ? .subviews : .all + ) // Keep the controls outside the clip's gesture so one button click // cannot also toggle playback through the surrounding media surface. .overlay { mediaControls } @@ -239,14 +390,16 @@ struct CloudWelcomeMediaCarousel: View { .onHover { hovering in withAnimation(.easeOut(duration: 0.15)) { isHoveringMedia = hovering } } - .overlay(shape.strokeBorder(Color.primary.opacity(0.12), lineWidth: 0.5)) + .overlay(shape.strokeBorder(Color.primary.opacity(showsFeatureList ? 0 : 0.12), lineWidth: 0.5)) } /// Autoplay: a pause button while the pointer is over the clip (a play button /// stays while paused). Manual: previous and next arrows on hover. @ViewBuilder private var mediaControls: some View { - if autoplays { + if showsFeatureList { + EmptyView() + } else if autoplays { if isHoveringMedia || playback.isPaused { controlButton( symbol: playback.isPaused ? "play.fill" : "pause.fill", @@ -299,7 +452,7 @@ struct CloudWelcomeMediaCarousel: View { @ViewBuilder private func slideMedia(_ slide: CloudWelcomeSlide) -> some View { if let url = mediaURL(slide) { - Player(url: url, playback: playback) + Player(url: url, playback: playback, reduceMotion: reduceMotion) } else { placeholder(slide) } @@ -363,46 +516,6 @@ struct CloudWelcomeMediaCarousel: View { .clipShape(Capsule(style: .continuous)) } - /// Dots variant: the current feature's bar spans its title and line (and - /// fills with the clip when autoplaying); the others are dots centered on - /// their title. The dot stretches into the bar as a row becomes current. - private func dotMarker(isCurrent: Bool, isHovered: Bool) -> some View { - ZStack { - if isCurrent { - railSegment(isCurrent: true) - .frame(width: 2.5) - .padding(.vertical, 8) - .transition(.scale(scale: 0.1, anchor: .center).combined(with: .opacity)) - } else { - Circle() - .fill(Color.primary.opacity(isHovered ? 0.5 : 0.25)) - .frame(width: 5, height: 5) - .transition(.opacity) - } - } - .frame(width: 5) - .frame(maxHeight: .infinity) - } - - /// A row's part of the rail. Manual: the selected one is lit. Autoplay: the - /// selected one fills top to bottom with its clip, then the next row takes over. - @ViewBuilder - private func railSegment(isCurrent: Bool) -> some View { - if isCurrent && autoplays && !reduceMotion { - Capsule(style: .continuous) - .fill(Color.primary.opacity(0.16)) - .overlay { - TimelineView(.animation(paused: playback.isPaused)) { context in - RailFill(fraction: progress(at: context.date), vertical: true) - } - } - .clipShape(Capsule(style: .continuous)) - } else { - Capsule(style: .continuous) - .fill(Color.primary.opacity(isCurrent ? 0.9 : 0.12)) - } - } - /// The clip's playhead when it is a movie, so the pill never drifts from /// what is on screen; the wall clock for a GIF or a placeholder. private func progress(at date: Date) -> Double { @@ -432,6 +545,7 @@ struct CloudWelcomeMediaCarousel: View { private struct AdvanceKey: Equatable { let slide: String + let revision: Int let isPaused: Bool let duration: Double let movieFailed: Bool @@ -442,7 +556,11 @@ struct CloudWelcomeMediaCarousel: View { } private func show(_ newIndex: Int) { - guard newIndex != index else { return } + // Picking the slide that's already up plays it again from the start. + guard newIndex != index else { + playback.restart() + return + } playback.detach() durationLoadFailed = false // Picking a slide plays it, even if the last one was paused. @@ -472,14 +590,16 @@ struct CloudWelcomeMediaCarousel: View { guard autoplays, !reduceMotion, !playback.isPaused, slides.count > 1, (!isMovie || movieFailed) else { return } let slideID = currentSlide.id let slideIndex = index + let revision = playback.revision let remaining = duration - playback.elapsed(at: Date()) if remaining > 0 { - await playback.waitForAdvance(after: remaining) + guard await playback.waitForAdvance(after: remaining) else { return } } // A pause, a new slide or a stale failed-movie callback can land just as // the timer ends. Only the task that scheduled this slide may advance it. guard !Task.isCancelled, !playback.isPaused, + playback.revision == revision, index == slideIndex, currentSlide.id == slideID else { return } @@ -620,19 +740,26 @@ struct CloudWelcomeMediaCarousel: View { private struct Player: NSViewRepresentable { let url: URL let playback: Playback + let reduceMotion: Bool func makeNSView(context: Context) -> NSView { + playback.setReduceMotion(reduceMotion) if url.pathExtension.lowercased() == "gif" { let view = NSImageView() view.image = NSImage(contentsOf: url) - view.animates = true + view.animates = !reduceMotion view.imageScaling = .scaleProportionallyUpOrDown return view } return MovieView(url: url, playback: playback) } - func updateNSView(_ nsView: NSView, context: Context) {} + func updateNSView(_ nsView: NSView, context: Context) { + playback.setReduceMotion(reduceMotion) + if let imageView = nsView as? NSImageView { + imageView.animates = !reduceMotion + } + } final class MovieView: NSView { private let player = AVQueuePlayer() @@ -754,8 +881,12 @@ extension CloudWelcomeMediaCarousel { @ObservationIgnored private let loopContinuation: AsyncStream.Continuation private weak var player: AVPlayer? private(set) var index = 0 - private(set) var isPaused = false + private var manuallyPaused = false + private var reduceMotion = false + var isPaused: Bool { manuallyPaused || reduceMotion } private(set) var movieFailed = false + /// A restarted slide invalidates its previous fallback deadline. + private(set) var revision = 0 private var pausedElapsed: Double? private var startedAt: Date? private var advanceTimer: CloudWelcomeAdvanceTimer? @@ -771,7 +902,7 @@ extension CloudWelcomeMediaCarousel { /// Waits for a cancellable one-shot presentation deadline. A dispatch /// timer is used instead of task sleep so changing slides tears down the /// deadline immediately and does not leave a sleeping UI task behind. - func waitForAdvance(after seconds: Double) async { + func waitForAdvance(after seconds: Double) async -> Bool { cancelAdvanceTimer() let (events, continuation) = AsyncStream.makeStream(bufferingPolicy: .bufferingNewest(1)) advanceContinuation = continuation @@ -798,6 +929,7 @@ extension CloudWelcomeMediaCarousel { }, onCancel: { [weak self] in Task { @MainActor in self?.cancelAdvanceTimer(ifGeneration: generation) } }) + return !Task.isCancelled && generation == advanceGeneration } private func cancelAdvanceTimer(ifGeneration generation: Int? = nil) { @@ -831,32 +963,60 @@ extension CloudWelcomeMediaCarousel { } func setPaused(_ paused: Bool) { - guard paused != isPaused else { return } + let wasPaused = isPaused + manuallyPaused = paused + updatePlaybackPause(wasPaused: wasPaused) + } + + /// Every media attachment and slide transition obeys Reduce Motion. + func setReduceMotion(_ enabled: Bool) { + guard enabled != reduceMotion else { return } + let wasPaused = isPaused + reduceMotion = enabled + if enabled { + startedAt = Date() + pausedElapsed = 0 + } + updatePlaybackPause(wasPaused: wasPaused) + if enabled { + player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) + } + } + + private func updatePlaybackPause(wasPaused: Bool) { + guard wasPaused != isPaused else { return } let now = Date() - if paused { + if isPaused { pausedElapsed = elapsed(at: now) - isPaused = true player?.pause() } else { if let held = pausedElapsed { startedAt = now.addingTimeInterval(-held) } pausedElapsed = nil - isPaused = false player?.play() } } /// Starts the new slide's playhead and clears any pause carried by the old slide. - func startNewSlide(at newIndex: Int? = nil) { + func startNewSlide(at newIndex: Int? = nil, resetsFailure: Bool = true) { + cancelAdvanceTimer() + revision += 1 if let newIndex { index = newIndex } - movieFailed = false + if resetsFailure { movieFailed = false } startedAt = Date() - pausedElapsed = nil - isPaused = false - player?.play() + manuallyPaused = false + pausedElapsed = reduceMotion ? 0 : nil + if isPaused { player?.pause() } else { player?.play() } + } + + /// Plays the current movie again from its first frame. + func restart() { + cancelAdvanceTimer() + player?.seek(to: .zero, toleranceBefore: .zero, toleranceAfter: .zero) + startNewSlide(resetsFailure: false) } /// Returns elapsed playhead time, including a held value while paused. diff --git a/Sources/Cloud/CloudWelcomeView.swift b/Sources/Cloud/CloudWelcomeView.swift index 5bfae1877fd6..c90546676446 100644 --- a/Sources/Cloud/CloudWelcomeView.swift +++ b/Sources/Cloud/CloudWelcomeView.swift @@ -12,13 +12,14 @@ enum CloudWelcomeNextStep: Equatable { case openCloud static func resolve(isAuthenticated: Bool, isPlanKnown: Bool, isPro: Bool) -> CloudWelcomeNextStep { - guard isAuthenticated else { return .signIn } + // Signed out reads like Free: Upgrade to Pro (the pricing page signs in). + guard isAuthenticated else { return .upgrade } guard isPlanKnown else { return .openCloud } return isPro ? .enable : .upgrade } } -/// "Your work, wherever you go": shown once on the 0.65.1 launch for users who +/// "Cloud machines for your agents": shown once on the 0.65.1 launch for users who /// can use Cloud. A compact introduction above feature clips and the next step. /// /// Takes plain values (no app objects) so the same view renders in the app and @@ -35,14 +36,31 @@ struct CloudWelcomeView: View { var sliderAutoplays = true var sliderShowsFeatureList = true var sliderListUsesDots = false + /// Launch card: the selected Headline preview ships by default. + var posterHeadline: String? = CloudWelcomeView.headline + /// Launch card: the user's New Cloud Machine shortcut for the Create row. + var newMachineShortcut: String? = "⇧⌘Y" + /// Launch card variations (debug comparisons): title in the poster band or + /// under the clip, the icon in the band, a New badge, and a footer with + /// Not Now beside the action. + var posterShowsTitle = true + var posterShowsIcon = true + var showsNewBadge = false + var footerHasNotNow = false + @State private var isHoveringPrimary = false + @State private var isHoveringNotNow = false static let windowWidth: CGFloat = 580 - /// The list layouts put the features beside the clip, so they need more room. - static let listWindowWidth: CGFloat = 680 + nonisolated static let headline = String(localized: "cloud.welcome.headline", defaultValue: "Cloud machines for your agents") + /// The launch layout: the clip poster on top, the features under it. + static let listWindowWidth: CGFloat = CloudWelcomeMediaCarousel.posterWidth var body: some View { let hasMedia = showsMedia && CloudWelcomeSlide.all.contains { mediaURL($0) != nil } - return VStack(spacing: 0) { + if hasMedia && sliderShowsFeatureList { + return AnyView(launchBody) + } + return AnyView(VStack(spacing: 0) { CloudWelcomeHeader() .frame(maxWidth: .infinity, alignment: .center) .padding(.horizontal, 24) @@ -58,11 +76,159 @@ struct CloudWelcomeView: View { .padding(.horizontal, 28) .padding(.bottom, 22) } - .frame(width: sliderShowsFeatureList && hasMedia ? Self.listWindowWidth : Self.windowWidth) + .frame(width: Self.windowWidth) .background(windowBackground) + .accessibilityIdentifier("CloudWelcomeWindow")) + } + + /// The 0.65.1 launch card: the clip poster (with the title in its band), + /// the five features, then one full-width action with the plan line under + /// it. The × in the corner (and Escape) is Not Now. + private var launchBody: some View { + VStack(spacing: 0) { + CloudWelcomeMediaCarousel( + slides: CloudWelcomeSlide.all, + mediaURL: mediaURL, + autoplays: sliderAutoplays, + showsFeatureList: true, + listUsesDots: false, + posterHeadline: posterHeadline, + newMachineShortcut: newMachineShortcut, + posterShowsTitle: posterShowsTitle, + posterShowsIcon: posterShowsIcon, + showsNewBadge: showsNewBadge, + cardWidth: launchCardWidth + ) + if footerHasNotNow { + launchFooterWithNotNow + } else { + VStack(spacing: 9) { + Button { + onNext(nextStep) + } label: { + Text(primaryLabel) + .cmuxFont(size: 13, weight: .semibold) + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + .frame(height: 36) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color.accentColor) + // Darkens a step under the pointer, like a native button. + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(Color.black.opacity(isHoveringPrimary ? 0.14 : 0)) + ) + ) + .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + } + .buttonStyle(.plain) + .onHover { hovering in + withAnimation(.easeOut(duration: 0.12)) { isHoveringPrimary = hovering } + } + // No Return shortcut: the window appears on its own at launch, and a + // Return meant for the terminal must not enable Cloud or open pricing. + .accessibilityIdentifier("CloudWelcomePrimaryButton") + Text(noteLines.joined(separator: " ")) + .cmuxFont(size: 11) + .foregroundStyle(.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.horizontal, 24) + .padding(.top, 20) + .padding(.bottom, 20) + } + } + .frame(width: launchCardWidth) + .overlay(alignment: .topTrailing) { + Button(action: onNotNow) { + Image(systemName: "xmark") + .cmuxFont(size: 9, weight: .bold) + .foregroundStyle(.white.opacity(0.8)) + .frame(width: 24, height: 24) + .background(.ultraThinMaterial, in: Circle()) + .environment(\.colorScheme, .dark) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .keyboardShortcut(.cancelAction) + .help(String(localized: "common.notNow", defaultValue: "Not Now")) + .accessibilityLabel(String(localized: "common.notNow", defaultValue: "Not Now")) + .accessibilityIdentifier("CloudWelcomeNotNowButton") + .padding(12) + } + // An opaque card (no glass): the same colour as the window behind it. + .background(Color(nsColor: Self.launchCardColor)) .accessibilityIdentifier("CloudWelcomeWindow") } + /// The Not Now footer needs room for the plan line and two buttons on one row. + private var launchCardWidth: CGFloat { + footerHasNotNow ? 480 : Self.listWindowWidth + } + + /// The plan line on the left, Not Now and the action on the right, under a hairline. + private var launchFooterWithNotNow: some View { + VStack(spacing: 0) { + Divider() + HStack(spacing: 6) { + Text(noteLines.joined(separator: " ")) + .cmuxFont(size: 11) + .foregroundStyle(.primary) + .lineLimit(1) + .minimumScaleFactor(0.8) + Spacer(minLength: 6) + Button(action: onNotNow) { + Text(String(localized: "common.notNow", defaultValue: "Not Now")) + .cmuxFont(size: 13, weight: .medium) + .padding(.horizontal, 12) + .frame(height: 30) + .background(Capsule(style: .continuous).fill(Color.primary.opacity(isHoveringNotNow ? 0.16 : 0.1))) + .contentShape(Capsule(style: .continuous)) + } + .buttonStyle(.plain) + .fixedSize() + .onHover { hovering in + withAnimation(.easeOut(duration: 0.12)) { isHoveringNotNow = hovering } + } + Button { + onNext(nextStep) + } label: { + Text(primaryLabel) + .cmuxFont(size: 13, weight: .semibold) + .foregroundStyle(.white) + .padding(.horizontal, 13) + .frame(height: 30) + .background( + Capsule(style: .continuous) + .fill(Color.accentColor) + .overlay(Capsule(style: .continuous).fill(Color.black.opacity(isHoveringPrimary ? 0.14 : 0))) + ) + .contentShape(Capsule(style: .continuous)) + } + .buttonStyle(.plain) + .fixedSize() + .onHover { hovering in + withAnimation(.easeOut(duration: 0.12)) { isHoveringPrimary = hovering } + } + .accessibilityIdentifier("CloudWelcomePrimaryButton") + } + .padding(.horizontal, 24) + .padding(.vertical, 16) + } + .padding(.top, 14) + } + + /// The launch card: a lifted near-black on dark, near-white on light. + static var launchCardColor: NSColor { + NSColor(name: nil) { appearance in + appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua + ? NSColor(white: 0.11, alpha: 1) + : NSColor(white: 0.98, alpha: 1) + } + } + /// On macOS 26 the window hosts this view inside an NSGlassEffectView (the /// controller does that), so the glass is the window itself: it fills the /// window to its own corners and never lenses this content. SwiftUI's @@ -174,7 +340,7 @@ struct CloudWelcomeView: View { ) switch nextStep { case .enable: - return [String(localized: "cloud.welcome.note.included", defaultValue: "Included in your plan.")] + return [String(localized: "cloud.welcome.note.alreadyIncluded", defaultValue: "Already included in your plan.")] case .signIn, .upgrade, .openCloud: return [available] } diff --git a/Sources/Cloud/CloudWelcomeWindowController.swift b/Sources/Cloud/CloudWelcomeWindowController.swift index 2fb04c6eee22..f5b771711e9c 100644 --- a/Sources/Cloud/CloudWelcomeWindowController.swift +++ b/Sources/Cloud/CloudWelcomeWindowController.swift @@ -43,33 +43,97 @@ final class CloudWelcomeWindowController: NSObject, NSWindowDelegate { /// Presents at launch when it applies, and marks it seen on the way so a /// quit or crash while it is open does not show it again. func presentIfNeeded(over parent: NSWindow?, defaults: UserDefaults = .standard) { - guard !didConsiderLaunchPresentation else { return } - didConsiderLaunchPresentation = true let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" - guard Self.shouldPresentAutomatically( + let environment = ProcessInfo.processInfo.environment + #if DEBUG + let isDebugBuild = true + #else + let isDebugBuild = false + #endif + guard claimLaunchPresentation( defaults: defaults, appVersion: appVersion, - policy: ManagedDevicePolicy() + policy: ManagedDevicePolicy(), + isDebugBuild: isDebugBuild, + isRunningUnderXCTest: MacSentryStartupPolicy.isRunningUnderXCTest(environment: environment), + isUITestMode: environment["CMUX_UI_TEST_MODE"] == "1" ) else { return } - Self.markCampaignSeen(in: defaults) present(over: parent) } - /// Help and launch share the same feature-list layout. - func present(over parent: NSWindow?, sliderShowsFeatureList: Bool = true, sliderListUsesDots: Bool = false) { + /// Claims the release announcement synchronously before creating its window. + /// Overlapping main-window callbacks and a later launch share the persisted + /// marker; an ineligible first window still consumes this launch's decision. + func claimLaunchPresentation( + defaults: UserDefaults, + appVersion: String, + policy: ManagedDevicePolicy, + isDebugBuild: Bool = false, + isRunningUnderXCTest: Bool = false, + isUITestMode: Bool = false + ) -> Bool { + guard !didConsiderLaunchPresentation else { return false } + didConsiderLaunchPresentation = true + guard Self.shouldPresentAutomatically( + defaults: defaults, + appVersion: appVersion, + policy: policy, + isDebugBuild: isDebugBuild, + isRunningUnderXCTest: isRunningUnderXCTest, + isUITestMode: isUITestMode + ) else { return false } + Self.markCampaignSeen(in: defaults) + return true + } + + /// Help deliberately reopens the same headline card without consuming or + /// resetting the automatic release announcement's persistence marker. + func present( + over parent: NSWindow?, + sliderShowsFeatureList: Bool = true, + sliderListUsesDots: Bool = false, + posterHeadline: String? = CloudWelcomeView.headline, + posterShowsTitle: Bool = true, + posterShowsIcon: Bool = true, + showsNewBadge: Bool = false, + footerHasNotNow: Bool = false + ) { window?.close() - let window = makeWindow(sliderShowsFeatureList: sliderShowsFeatureList, sliderListUsesDots: sliderListUsesDots) + let window = makeWindow( + sliderShowsFeatureList: sliderShowsFeatureList, + sliderListUsesDots: sliderListUsesDots, + posterHeadline: posterHeadline, + posterShowsTitle: posterShowsTitle, + posterShowsIcon: posterShowsIcon, + showsNewBadge: showsNewBadge, + footerHasNotNow: footerHasNotNow + ) self.window = window position(window, over: parent) NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) } - private func makeWindow(sliderShowsFeatureList: Bool, sliderListUsesDots: Bool) -> NSWindow { + private func makeWindow( + sliderShowsFeatureList: Bool, + sliderListUsesDots: Bool, + posterHeadline: String?, + posterShowsTitle: Bool, + posterShowsIcon: Bool, + showsNewBadge: Bool, + footerHasNotNow: Bool + ) -> NSWindow { + let shortcut = KeyboardShortcutSettings.shortcut(for: .newCloudMachine) let rootView = CloudWelcomeAccountView( accountFlow: AppDelegate.shared?.auth?.accountFlow, sliderShowsFeatureList: sliderShowsFeatureList, sliderListUsesDots: sliderListUsesDots, + posterHeadline: posterHeadline, + newMachineShortcut: shortcut.isUnbound ? nil : shortcut.displayString, + posterShowsTitle: posterShowsTitle, + posterShowsIcon: posterShowsIcon, + showsNewBadge: showsNewBadge, + footerHasNotNow: footerHasNotNow, onNotNow: { [weak self] in self?.dismiss() }, onNext: { [weak self] step in self?.perform(step) } ) @@ -87,7 +151,7 @@ final class CloudWelcomeWindowController: NSObject, NSWindowDelegate { defer: false ) window.identifier = NSUserInterfaceItemIdentifier("cmux.cloud.welcome") - window.title = String(localized: "cloud.welcome.title", defaultValue: "Your work, wherever you go") + window.title = String(localized: "cloud.welcome.title.eyebrow", defaultValue: "cmux cloud") window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.isMovableByWindowBackground = true @@ -97,9 +161,27 @@ final class CloudWelcomeWindowController: NSObject, NSWindowDelegate { window.backgroundColor = .clear window.standardWindowButton(.miniaturizeButton)?.isEnabled = false window.standardWindowButton(.zoomButton)?.isEnabled = false + if sliderShowsFeatureList { + // The launch card's own × (and Escape) is Not Now; Cmd+W still closes. + for kind in [NSWindow.ButtonType.closeButton, .miniaturizeButton, .zoomButton] { + window.standardWindowButton(kind)?.isHidden = true + } + } window.contentView = hosting + if sliderShowsFeatureList { + // The launch card is opaque: the window takes the card's colour. + window.isOpaque = true + window.backgroundColor = CloudWelcomeView.launchCardColor + // Hosted in a plain container: as the window's own content view the + // hosting view reserves the hidden titlebar's height above the clip. + let container = NSView(frame: NSRect(origin: .zero, size: size)) + hosting.frame = container.bounds + hosting.autoresizingMask = [.width, .height] + container.addSubview(hosting) + window.contentView = container + } #if compiler(>=6.2) - if #available(macOS 26.0, *) { + if !sliderShowsFeatureList, #available(macOS 26.0, *) { // The window is the glass: content goes inside it, edge to edge, and the // window's own frame rounds the corners. let glass = NSGlassEffectView() @@ -161,6 +243,12 @@ private struct CloudWelcomeAccountView: View { let accountFlow: HostAccountFlow? let sliderShowsFeatureList: Bool let sliderListUsesDots: Bool + let posterHeadline: String? + let newMachineShortcut: String? + let posterShowsTitle: Bool + let posterShowsIcon: Bool + let showsNewBadge: Bool + let footerHasNotNow: Bool let onNotNow: () -> Void let onNext: (CloudWelcomeNextStep) -> Void @@ -174,7 +262,13 @@ private struct CloudWelcomeAccountView: View { onNotNow: onNotNow, onNext: onNext, sliderShowsFeatureList: sliderShowsFeatureList, - sliderListUsesDots: sliderListUsesDots + sliderListUsesDots: sliderListUsesDots, + posterHeadline: posterHeadline, + newMachineShortcut: newMachineShortcut, + posterShowsTitle: posterShowsTitle, + posterShowsIcon: posterShowsIcon, + showsNewBadge: showsNewBadge, + footerHasNotNow: footerHasNotNow ) .task { // The plan decides between Upgrade and Enable; ask once on show. diff --git a/Sources/Debug/CloudWelcomeDebugMenu.swift b/Sources/Debug/CloudWelcomeDebugMenu.swift index b6400f1dbb88..1b55dbab9c8f 100644 --- a/Sources/Debug/CloudWelcomeDebugMenu.swift +++ b/Sources/Debug/CloudWelcomeDebugMenu.swift @@ -3,23 +3,25 @@ import AppKit import SwiftUI extension cmuxApp { - /// Debug-only Help menu entries for comparing the Cloud welcome layouts. + /// Deliberately reopen the shipped welcome for inspection in a debug build. @ViewBuilder var cloudWelcomeDebugMenuItems: some View { Button(String(localized: "debug.menu.showCloudWelcome", defaultValue: "Show Cloud Welcome…")) { + AppDelegate.shared?.cloudWelcomeWindowController.present(over: NSApp.mainWindow) + } + Button(String(localized: "debug.menu.showCloudWelcomeFBadge", defaultValue: "Show Cloud Welcome (F, New Badge)…")) { AppDelegate.shared?.cloudWelcomeWindowController.present( over: NSApp.mainWindow, - sliderShowsFeatureList: false + posterShowsTitle: false, + showsNewBadge: true, + footerHasNotNow: true ) } - Button(String(localized: "debug.menu.showCloudWelcomeList", defaultValue: "Show Cloud Welcome (List)…")) { - AppDelegate.shared?.cloudWelcomeWindowController.present(over: NSApp.mainWindow, sliderShowsFeatureList: true) - } - Button(String(localized: "debug.menu.showCloudWelcomeListDots", defaultValue: "Show Cloud Welcome (List + Dots)…")) { + Button(String(localized: "debug.menu.showCloudWelcomeFOnClip", defaultValue: "Show Cloud Welcome (F, On Clip)…")) { AppDelegate.shared?.cloudWelcomeWindowController.present( over: NSApp.mainWindow, - sliderShowsFeatureList: true, - sliderListUsesDots: true + posterShowsIcon: false, + footerHasNotNow: true ) } } diff --git a/Sources/WindowDecorationsController.swift b/Sources/WindowDecorationsController.swift index e1c9dfaea78e..134f7d8fe0b8 100644 --- a/Sources/WindowDecorationsController.swift +++ b/Sources/WindowDecorationsController.swift @@ -479,6 +479,10 @@ final class WindowDecorationsController { if window.isSheet { return true } + // The Cloud welcome's own × (and Escape) closes it. + if window.identifier?.rawValue == "cmux.cloud.welcome" { + return true + } if window.styleMask.contains(.docModalWindow) { return true } diff --git a/Sources/WindowToolbarController.swift b/Sources/WindowToolbarController.swift index 00ebb7b820bb..a26994ac6fbb 100644 --- a/Sources/WindowToolbarController.swift +++ b/Sources/WindowToolbarController.swift @@ -170,6 +170,9 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { private func attach(to window: NSWindow) { guard window.toolbar == nil else { return } + // The Cloud welcome is a glass card, not a workspace window: a toolbar + // would turn its titlebar into a toolbar band and outline the card. + guard window.identifier?.rawValue != "cmux.cloud.welcome" else { return } guard !WorkspacePresentationModeSettings.isMinimal() else { return } let toolbar = NSToolbar(identifier: NSToolbar.Identifier("cmux.toolbar")) toolbar.delegate = self diff --git a/cmuxTests/CloudWelcomeTests.swift b/cmuxTests/CloudWelcomeTests.swift index 296a74f5f29b..b769e84f3cda 100644 --- a/cmuxTests/CloudWelcomeTests.swift +++ b/cmuxTests/CloudWelcomeTests.swift @@ -1,4 +1,5 @@ import AppKit +import AVFoundation import CmuxSettings import Testing @@ -10,6 +11,140 @@ import Testing @Suite("Cloud welcome") struct CloudWelcomeTests { + @Test("the shipped welcome selects the approved headline preview") + @MainActor + func launchUsesHeadlinePreview() { + let view = CloudWelcomeView(nextStep: .upgrade, onNotNow: {}, onNext: { _ in }) + #expect(view.posterHeadline == String(localized: "cloud.welcome.headline", defaultValue: "Cloud machines for your agents")) + #expect(view.sliderShowsFeatureList) + #expect(view.posterShowsTitle) + #expect(view.posterShowsIcon) + #expect(!view.showsNewBadge) + #expect(!view.footerHasNotNow) + let carousel = CloudWelcomeMediaCarousel(slides: CloudWelcomeSlide.all, mediaURL: { _ in nil }) + #expect(carousel.posterHeadline == view.posterHeadline) + } + + @Test("overlapping launch triggers, dismissal and relaunch present one campaign") + @MainActor + func launchClaimIsPersistedBeforePresentation() throws { + let suiteName = "CloudWelcomeTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let policy = ManagedDevicePolicy(defaults: defaults, releaseDomainDefaults: nil, forcedObject: { _, _ in nil }) + let controller = CloudWelcomeWindowController() + + #expect(controller.claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: policy)) + // The marker exists before the caller creates any window or awaits work. + #expect(defaults.string(forKey: CloudWelcomeWindowController.seenVersionDefaultsKey) == "0.65.1") + #expect(!controller.claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: policy)) + controller.dismiss() + #expect(!controller.claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: policy)) + + // A fresh controller and defaults handle model another launch/process. + let relaunchedDefaults = try #require(UserDefaults(suiteName: suiteName)) + let relaunched = CloudWelcomeWindowController() + #expect(!relaunched.claimLaunchPresentation(defaults: relaunchedDefaults, appVersion: "0.65.1", policy: policy)) + } + + @Test("users who saw the 0.65.0 welcome still get the 0.65.1 card") + @MainActor + func earlierWelcomeDoesNotSuppressCampaign() throws { + let suiteName = "CloudWelcomeTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let policy = ManagedDevicePolicy(defaults: defaults, releaseDomainDefaults: nil, forcedObject: { _, _ in nil }) + // 0.65.0 recorded its welcome under this boolean key. + defaults.set(true, forKey: "cmux.cloud.welcome.seen") + #expect(CloudWelcomeWindowController().claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: policy)) + } + + @Test("a blocked first window waits for the next launch, even when policy changes") + @MainActor + func blockedLaunchDoesNotReconsiderOrConsumeCampaign() throws { + let suiteName = "CloudWelcomeTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let blocked = ManagedDevicePolicy(defaults: defaults, releaseDomainDefaults: nil) { _, key in + key == ManagedDevicePolicyKey.disableCloud.rawValue ? true : nil + } + let allowed = ManagedDevicePolicy(defaults: defaults, releaseDomainDefaults: nil, forcedObject: { _, _ in nil }) + let controller = CloudWelcomeWindowController() + #expect(!controller.claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: blocked)) + #expect(!controller.claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: allowed)) + #expect(defaults.object(forKey: CloudWelcomeWindowController.seenVersionDefaultsKey) == nil) + #expect(CloudWelcomeWindowController().claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: allowed)) + } + + @Test("ineligible release and debug/test launches leave the campaign available", arguments: ["0.65.0", "0.65.2", "0.65.1-nightly", "0.65.1"]) + @MainActor + func launchClaimEnforcesReleaseAndRuntime(appVersion: String) throws { + let suiteName = "CloudWelcomeTests.\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let policy = ManagedDevicePolicy(defaults: defaults, releaseDomainDefaults: nil, forcedObject: { _, _ in nil }) + let modes = appVersion == "0.65.1" ? [(true, false, false), (false, true, false), (false, false, true)] : [(false, false, false)] + for (debug, xctest, uiTest) in modes { + let controller = CloudWelcomeWindowController() + #expect(!controller.claimLaunchPresentation( + defaults: defaults, appVersion: appVersion, policy: policy, + isDebugBuild: debug, isRunningUnderXCTest: xctest, isUITestMode: uiTest + )) + } + #expect(defaults.object(forKey: CloudWelcomeWindowController.seenVersionDefaultsKey) == nil) + #expect(CloudWelcomeWindowController().claimLaunchPresentation(defaults: defaults, appVersion: "0.65.1", policy: policy)) + } + + @Test("Reduce Motion holds movies through selection, restart and attachment") + @MainActor + func reducedMotionOwnsPlayback() { + let playback = CloudWelcomeMediaCarousel.Playback() + let player = AVPlayer() + playback.attach(player) + playback.setReduceMotion(true) + #expect(playback.isPaused) + playback.startNewSlide(at: 1) + playback.restart() + playback.setPaused(false) + #expect(playback.isPaused) + let replacement = AVPlayer() + playback.attach(replacement) + playback.setReduceMotion(false) + #expect(!playback.isPaused) + playback.setPaused(true) + playback.setReduceMotion(true) + playback.setReduceMotion(false) + #expect(playback.isPaused) + } + + @Test("a cancelled fallback deadline cannot advance the slide") + @MainActor + func cancelledDeadlineDoesNotAdvance() async { + let playback = CloudWelcomeMediaCarousel.Playback() + let deadline = Task { await playback.waitForAdvance(after: 60) } + await Task.yield() + deadline.cancel() + #expect(await deadline.value == false) + } + + @Test("restarting a failed movie preserves its fallback transition") + @MainActor + func restartingFailedMovieKeepsFailureState() async { + let playback = CloudWelcomeMediaCarousel.Playback() + let player = AVPlayer() + playback.attach(player) + playback.didFail(player) + #expect(playback.movieFailed) + let deadline = Task { await playback.waitForAdvance(after: 60) } + await Task.yield() + let revision = playback.revision + playback.restart() + #expect(playback.movieFailed) + #expect(playback.revision == revision + 1) + deadline.cancel() + #expect(await deadline.value == false) + } + @Test("welcome includes Cloud on and off; only MDM blocks it", arguments: [nil, false, true] as [Bool?], [false, true]) func welcomeIgnoresActivation(cloudIsOn: Bool?, blockedByMDM: Bool) throws { let suiteName = "CloudWelcomeTests.\(UUID().uuidString)" @@ -126,8 +261,9 @@ struct CloudWelcomeTests { @Test("the one prominent button is the next step for this account") func nextStepFollowsSignInAndPlan() { - #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: false, isPro: false) == .signIn) - #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: true, isPro: true) == .signIn) + // Signed out reads like Free: Upgrade to Pro. + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: false, isPro: false) == .upgrade) + #expect(CloudWelcomeNextStep.resolve(isAuthenticated: false, isPlanKnown: true, isPro: true) == .upgrade) #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: false, isPro: false) == .openCloud) #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: true, isPro: false) == .upgrade) #expect(CloudWelcomeNextStep.resolve(isAuthenticated: true, isPlanKnown: true, isPro: true) == .enable) @@ -135,7 +271,7 @@ struct CloudWelcomeTests { @Test("feature clips keep their stable product order") func featureClipIDsAreStable() { - #expect(CloudWelcomeSlide.all.map(\.id) == ["spin-up", "keeps-running", "displays", "invite", "team"]) + #expect(CloudWelcomeSlide.all.map(\.id) == ["spin-up", "displays", "invite", "team", "keeps-running"]) #expect(Set(CloudWelcomeSlide.all.map(\.id)).count == CloudWelcomeSlide.all.count) }