diff --git a/crates/config/src/settings_schema.rs b/crates/config/src/settings_schema.rs index ce8fc54022..72bf457aa6 100644 --- a/crates/config/src/settings_schema.rs +++ b/crates/config/src/settings_schema.rs @@ -221,11 +221,6 @@ const DEFAULT_MODE: &[SettingOption] = &[ ), ]; -const OCEAN_TREATMENT: &[SettingOption] = &[ - SettingOption::new("deepsea", "", "ConfigChoiceDetailOceanDeepsea"), - SettingOption::new("flat", "", "ConfigChoiceDetailOceanFlat"), -]; - const FOCUS_TEXTURE: &[SettingOption] = &[ SettingOption::new("off", "ConfigValueOff", ""), SettingOption::new("scrim", "", ""), @@ -396,17 +391,6 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[ "ConfigHintBackgroundColor", ), ), - def( - "ocean_treatment", - SettingKind::Enum(OCEAN_TREATMENT), - "flat", - ui( - TAB_APPEARANCE, - "display", - "ConfigLabelOceanTreatment", - "ConfigHintOceanTreatment", - ), - ), // No sentence anywhere justifies this prototype toggle, so it keeps its // declaration and loses its row. def( diff --git a/crates/lane/src/lib.rs b/crates/lane/src/lib.rs index e9e385e8ab..0b4010259d 100644 --- a/crates/lane/src/lib.rs +++ b/crates/lane/src/lib.rs @@ -25,4 +25,6 @@ pub use runtime::{ InlineRuntime, LaneLogProxySpec, LaneStartSpec, RuntimeBackend, RuntimeBackendKind, TmuxRuntime, backend_for, resolve_backend, run_lane_log_proxy, }; -pub use worktree::{WorktreeProvision, provision_worktree, remove_worktree_if_expired}; +pub use worktree::{ + ProvisionedWorktree, WorktreeProvision, provision_worktree, remove_worktree_if_expired, +}; diff --git a/crates/tui/assets/mark-48.png b/crates/tui/assets/mark-48.png index cb290fe27c..4bcdde2315 100644 Binary files a/crates/tui/assets/mark-48.png and b/crates/tui/assets/mark-48.png differ diff --git a/crates/tui/assets/mark-96.png b/crates/tui/assets/mark-96.png index 9fa22f8f0d..2a0afb5124 100644 Binary files a/crates/tui/assets/mark-96.png and b/crates/tui/assets/mark-96.png differ diff --git a/crates/tui/locales/ca.json b/crates/tui/locales/ca.json index f78307df25..11e475ff9a 100644 --- a/crates/tui/locales/ca.json +++ b/crates/tui/locales/ca.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Tema", "ConfigLabelLocale": "Idioma", "ConfigLabelBackground": "Fons", - "ConfigLabelOceanTreatment": "Tractament Ocean", "ConfigLabelWorkSurfacePlacement": "Posició de la barra lateral", "ConfigLabelTopHeight": "Alçada de la barra superior", "ConfigLabelSideWidth": "Amplada de la barra lateral", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "consultor", "SubagentsRoleCustom": "personalitzat", "HelpUnknownCommand": "Comanda desconeguda: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Model:", "HomeMode": "Mode:", "HomeWorkspace": "Espai de treball:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "proveïdor configurat", "LaunchProviderSetupShort": "configura proveïdor", "LaunchMenuChangelog": "Registre de canvis", - "LaunchWorktreePrompt": "Anomena la branca/worktree, o prem Enter per a un nom automàtic.", + "LaunchWorktreePrompt": "Anomena la branca/worktree, o prem Enter per a un nom automàtic. Esc per tornar.", "LaunchWorktreeNeedsGit": "Un worktree nou requereix un repositori Git.", "LaunchWorktreeNameLabel": "nom del worktree", "LaunchHintMove": "mou", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} sessions desades", "LaunchCreatingWorktree": "Creant el worktree…", "LaunchWorktreeFailed": "El worktree ha fallat: {error}", + "LaunchWorktreeCreated": "Nou worktree {path} a la branca {branch}", "LaunchNoSavedSessions": "No hi ha sessions desades per a aquest espai de treball.", "LaunchComposerHint": "Enter envia · Shift+Enter línia nova · Esc torna", "LaunchNoModelConnected": "cap model connectat", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Accés a credencials externes revocat per a {provider}", "ProviderExternalRevokeFailedToast": "No s'ha pogut revocar l'accés a credencials externes: {error}", "ThemeSurfaceTitle": "tema · previsualització en directe", - "ThemeTreatmentDeepseaUnavailable": "Tractament Deepsea no disponible — el terminal controla el fons", - "ThemeTreatmentFlatActive": "Tractament Flat — actiu", - "ThemeTreatmentDeepseaActive": "Tractament Deepsea — actiu", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "membres", "FleetRosterTabSetup": "configuració", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Manté ocult el raonament del model; respostes i eines continuen visibles.", "ConfigChoiceDetailThinkingHighlightOn": "Omple el fons del raonament del model.", "ConfigChoiceDetailThinkingHighlightOff": "Manté el rail discontinu i el text en cursiva sense fons ple.", - "ConfigChoiceDetailOceanDeepsea": "Usa un únic camp de color continu de l'oceà.", - "ConfigChoiceDetailOceanFlat": "Usa un únic color de fons pla.", "ConfigHintModel": "model de la ruta en viu d'aquesta sessió; Enter obre /model", "ConfigHintFastModel": "l'usen l'encaminament Auto i model_strength=faster dels agents quan aquest proveïdor té un germà conegut", "ConfigHintProvider": "proveïdor de la ruta en viu d'aquesta sessió; Enter obre /provider (credencial, model i endpoint canvien junts)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "silencia el crom de la transcripció i el detall de les eines; independent del moviment en viu", "ConfigHintLowMotion": "activat sobreescriu el moviment de l'estat en viu; la sortida del model no canvia", "ConfigHintFancyAnimations": "activat anima amb fidelitat l'estat en viu d'eines, estat i oceà", - "ConfigHintOceanTreatment": "deepsea | flat (aparença; independent del moviment)", "ConfigHintShowThinking": "mostra o amaga el raonament del model al xat; les llistes de tasques continuen concises", "ConfigHintThinkingDefaultExpanded": "expandeix el raonament del model per defecte; Espai continua commutant cada bloc", "ConfigHintThinkingPreviewLines": "files de previsualització del pensament completat plegat (predeterminat 2; 0=només capçalera; 10=bolcat antic)", diff --git a/crates/tui/locales/de.json b/crates/tui/locales/de.json index 2970704094..2ae244c78f 100644 --- a/crates/tui/locales/de.json +++ b/crates/tui/locales/de.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Theme", "ConfigLabelLocale": "Sprache", "ConfigLabelBackground": "Hintergrund", - "ConfigLabelOceanTreatment": "Ocean-Darstellung", "ConfigLabelWorkSurfacePlacement": "Seitenleisten-Position", "ConfigLabelTopHeight": "Höhe der oberen Leiste", "ConfigLabelSideWidth": "Breite der Seitenleiste", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "Berater", "SubagentsRoleCustom": "benutzerdefiniert", "HelpUnknownCommand": "Unbekannter Befehl: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Modell:", "HomeMode": "Modus:", "HomeWorkspace": "Workspace:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "Anbieter eingerichtet", "LaunchProviderSetupShort": "Anbieter einrichten", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Branch/Worktree benennen oder Enter für einen automatischen Namen.", + "LaunchWorktreePrompt": "Branch/Worktree benennen oder Enter für einen automatischen Namen. Esc zurück.", "LaunchWorktreeNeedsGit": "Neuer Worktree erfordert ein Git-Repository.", "LaunchWorktreeNameLabel": "Worktree-Name", "LaunchHintMove": "bewegen", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} gespeicherte Sitzungen", "LaunchCreatingWorktree": "Worktree wird erstellt…", "LaunchWorktreeFailed": "Worktree fehlgeschlagen: {error}", + "LaunchWorktreeCreated": "Neuer Worktree {path} auf Branch {branch}", "LaunchNoSavedSessions": "Keine gespeicherten Sitzungen für diesen Workspace.", "LaunchComposerHint": "Enter senden · Shift+Enter neue Zeile · Esc zurück", "LaunchNoModelConnected": "kein Modell verbunden", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Externer Zugangsdaten-Zugriff für {provider} widerrufen", "ProviderExternalRevokeFailedToast": "Externer Zugangsdaten-Zugriff wurde nicht widerrufen: {error}", "ThemeSurfaceTitle": "Theme · Live-Vorschau", - "ThemeTreatmentDeepseaUnavailable": "Treatment Deepsea nicht verfügbar — Terminal besitzt den Hintergrund", - "ThemeTreatmentFlatActive": "Treatment Flat — aktiv", - "ThemeTreatmentDeepseaActive": "Treatment Deepsea — aktiv", "FleetRosterHeaderLabel": "Pod", "FleetRosterTabRoster": "Mitglieder", "FleetRosterTabSetup": "Setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Hält das Reasoning des Modells verborgen; Antworten und Werkzeuge bleiben sichtbar.", "ConfigChoiceDetailThinkingHighlightOn": "Füllt den Hintergrund des Modell-Reasonings.", "ConfigChoiceDetailThinkingHighlightOff": "Behält die gestrichelte Leiste und den kursiven Text ohne gefüllten Hintergrund.", - "ConfigChoiceDetailOceanDeepsea": "Nutzt ein durchgehendes Ozean-Farbfeld.", - "ConfigChoiceDetailOceanFlat": "Nutzt eine einzelne flache Hintergrundfarbe.", "ConfigHintModel": "Modell der Live-Route dieser Sitzung; Enter öffnet /model", "ConfigHintFastModel": "wird von Auto-Routing und agentischem model_strength=faster genutzt, wenn dieser Anbieter ein bekanntes Schwestermodell hat", "ConfigHintProvider": "Anbieter der Live-Route dieser Sitzung; Enter öffnet /provider (Zugangsdaten, Modell und Endpunkt wechseln gemeinsam)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "beruhigt Transkript-Chrome und Werkzeugdetails; unabhängig von Live-Bewegung", "ConfigHintLowMotion": "an überschreibt Live-Bewegung; die Modellausgabe bleibt unverändert", "ConfigHintFancyAnimations": "an animiert wahrheitsgetreu den Live-Zustand von Werkzeugen, Status und Ozean", - "ConfigHintOceanTreatment": "deepsea | flat (Darstellung; unabhängig von Bewegung)", "ConfigHintShowThinking": "Modell-Reasoning im Chat zeigen oder verbergen; Aufgabenlisten bleiben knapp", "ConfigHintThinkingDefaultExpanded": "Modell-Reasoning standardmäßig ausklappen; Leertaste schaltet weiterhin jeden Block um", "ConfigHintThinkingPreviewLines": "Vorschauzeilen eingeklappter abgeschlossener Gedanken (Standard 2; 0=nur Kopfzeile; 10=älterer Dump)", diff --git a/crates/tui/locales/en.json b/crates/tui/locales/en.json index f456091566..539118bf6e 100644 --- a/crates/tui/locales/en.json +++ b/crates/tui/locales/en.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Theme", "ConfigLabelLocale": "Language", "ConfigLabelBackground": "Background", - "ConfigLabelOceanTreatment": "Ocean treatment", "ConfigLabelWorkSurfacePlacement": "Sidebar position", "ConfigLabelTopHeight": "Top bar height", "ConfigLabelSideWidth": "Side bar width", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "advisor", "SubagentsRoleCustom": "custom", "HelpUnknownCommand": "Unknown command: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Model:", "HomeMode": "Mode:", "HomeWorkspace": "Workspace:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "provider set", "LaunchProviderSetupShort": "provider setup", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Name the branch/worktree, or press Enter for an automatic name.", + "LaunchWorktreePrompt": "Name the branch/worktree, or press Enter for an automatic name. Esc back.", "LaunchWorktreeNeedsGit": "New worktree requires a Git repository.", "LaunchWorktreeNameLabel": "worktree name", "LaunchHintMove": "move", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "{count} saved sessions", "LaunchCreatingWorktree": "Creating worktree…", "LaunchWorktreeFailed": "Worktree failed: {error}", + "LaunchWorktreeCreated": "New worktree {path} on branch {branch}", "LaunchNoSavedSessions": "No saved sessions for this workspace.", "LaunchComposerHint": "Enter send · Shift+Enter new line · Esc back", "LaunchNoModelConnected": "no model connected", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "External credential access revoked for {provider}", "ProviderExternalRevokeFailedToast": "External credential access was not revoked: {error}", "ThemeSurfaceTitle": "theme · live preview", - "ThemeTreatmentDeepseaUnavailable": "Treatment Deepsea unavailable — Terminal owns the background", - "ThemeTreatmentFlatActive": "Treatment Flat — active", - "ThemeTreatmentDeepseaActive": "Treatment Deepsea — active", "FleetRosterHeaderLabel": "fleet", "FleetRosterTabRoster": "members", "FleetRosterTabSetup": "setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Keep model reasoning hidden; answers and tools remain visible.", "ConfigChoiceDetailThinkingHighlightOn": "Fill the model reasoning background.", "ConfigChoiceDetailThinkingHighlightOff": "Keep the dashed reasoning rail and italic text without a filled background.", - "ConfigChoiceDetailOceanDeepsea": "Use one continuous ocean color field.", - "ConfigChoiceDetailOceanFlat": "Use a single flat background color.", "ConfigHintModel": "live route model for this session; Enter opens /model", "ConfigHintFastModel": "used by Auto routing and agent model_strength=faster when this provider has a known sibling", "ConfigHintProvider": "live route provider for this session; Enter opens /provider (credential, model, and endpoint switch together)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "quietens transcript chrome and tool detail; independent of live motion", "ConfigHintLowMotion": "on overrides live-state motion; model output is unchanged", "ConfigHintFancyAnimations": "on animates truthful tool, status, and ocean live state", - "ConfigHintOceanTreatment": "deepsea | flat (appearance; independent of motion)", "ConfigHintShowThinking": "show or hide model reasoning in chat; task lists stay concise", "ConfigHintThinkingDefaultExpanded": "expand model reasoning by default; Space still toggles each block", "ConfigHintThinkingPreviewLines": "collapsed completed-thought preview rows (default 2; 0=header-only; 10=older dump)", diff --git a/crates/tui/locales/es-419.json b/crates/tui/locales/es-419.json index 24d12465e0..70eecd4d38 100644 --- a/crates/tui/locales/es-419.json +++ b/crates/tui/locales/es-419.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Tema", "ConfigLabelLocale": "Idioma", "ConfigLabelBackground": "Fondo", - "ConfigLabelOceanTreatment": "Tratamiento oceánico", "ConfigLabelWorkSurfacePlacement": "Posición de la barra lateral", "ConfigLabelTopHeight": "Altura de la barra superior", "ConfigLabelSideWidth": "Ancho de la barra lateral", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "consultor", "SubagentsRoleCustom": "personalizado", "HelpUnknownCommand": "Comando desconocido: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Modelo:", "HomeMode": "Modo:", "HomeWorkspace": "Espacio de trabajo:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "proveedor configurado", "LaunchProviderSetupShort": "configurar proveedor", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Nombra la rama/worktree, o presiona Enter para un nombre automático.", + "LaunchWorktreePrompt": "Nombra la rama/worktree, o presiona Enter para un nombre automático. Esc para volver.", "LaunchWorktreeNeedsGit": "Un worktree nuevo requiere un repositorio Git.", "LaunchWorktreeNameLabel": "nombre del worktree", "LaunchHintMove": "mover", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "{count} sesiones guardadas", "LaunchCreatingWorktree": "Creando worktree…", "LaunchWorktreeFailed": "El worktree falló: {error}", + "LaunchWorktreeCreated": "Nuevo worktree {path} en la rama {branch}", "LaunchNoSavedSessions": "No hay sesiones guardadas para este workspace.", "LaunchComposerHint": "Enter envía · Shift+Enter nueva línea · Esc vuelve", "LaunchNoModelConnected": "ningún modelo conectado", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "Se revocó el acceso a credenciales externas para {provider}", "ProviderExternalRevokeFailedToast": "No se revocó el acceso a credenciales externas: {error}", "ThemeSurfaceTitle": "tema · vista previa en vivo", - "ThemeTreatmentDeepseaUnavailable": "Tratamiento Deepsea no disponible — la terminal controla el fondo", - "ThemeTreatmentFlatActive": "Tratamiento Flat — activo", - "ThemeTreatmentDeepseaActive": "Tratamiento Deepsea — activo", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "miembros", "FleetRosterTabSetup": "setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Mantiene oculto el razonamiento del modelo; respuestas y herramientas siguen visibles.", "ConfigChoiceDetailThinkingHighlightOn": "Rellena el fondo del razonamiento del modelo.", "ConfigChoiceDetailThinkingHighlightOff": "Mantiene el riel punteado y el texto en cursiva sin fondo relleno.", - "ConfigChoiceDetailOceanDeepsea": "Usa un único campo de color continuo del océano.", - "ConfigChoiceDetailOceanFlat": "Usa un único color de fondo plano.", "ConfigHintModel": "modelo de la ruta en vivo de esta sesión; Enter abre /model", "ConfigHintFastModel": "lo usan el enrutamiento Auto y model_strength=faster de los agentes cuando este proveedor tiene un hermano conocido", "ConfigHintProvider": "proveedor de la ruta en vivo de esta sesión; Enter abre /provider (credencial, modelo y endpoint cambian juntos)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "silencia el cromo de la transcripción y el detalle de herramientas; independiente del movimiento en vivo", "ConfigHintLowMotion": "activado anula el movimiento del estado en vivo; la salida del modelo no cambia", "ConfigHintFancyAnimations": "activado anima con fidelidad el estado en vivo de herramientas, estado y océano", - "ConfigHintOceanTreatment": "deepsea | flat (apariencia; independiente del movimiento)", "ConfigHintShowThinking": "muestra u oculta el razonamiento del modelo en el chat; las listas de tareas siguen concisas", "ConfigHintThinkingDefaultExpanded": "expande el razonamiento del modelo de forma predeterminada; Espacio sigue alternando cada bloque", "ConfigHintThinkingPreviewLines": "filas de vista previa del pensamiento completado plegado (predeterminado 2; 0=solo encabezado; 10=volcado antiguo)", diff --git a/crates/tui/locales/fr.json b/crates/tui/locales/fr.json index 84142ea359..9658b94e72 100644 --- a/crates/tui/locales/fr.json +++ b/crates/tui/locales/fr.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Thème", "ConfigLabelLocale": "Langue", "ConfigLabelBackground": "Arrière-plan", - "ConfigLabelOceanTreatment": "Traitement océan", "ConfigLabelWorkSurfacePlacement": "Position de la barre latérale", "ConfigLabelTopHeight": "Hauteur de la barre supérieure", "ConfigLabelSideWidth": "Largeur de la barre latérale", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "consultant", "SubagentsRoleCustom": "personnalisé", "HelpUnknownCommand": "Commande inconnue : {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Modèle :", "HomeMode": "Mode :", "HomeWorkspace": "Workspace :", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "fournisseur configuré", "LaunchProviderSetupShort": "configurer fournisseur", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Nommez la branche/worktree, ou appuyez sur Enter pour un nom automatique.", + "LaunchWorktreePrompt": "Nommez la branche/worktree, ou appuyez sur Enter pour un nom automatique. Esc pour revenir.", "LaunchWorktreeNeedsGit": "Un nouveau worktree nécessite un dépôt Git.", "LaunchWorktreeNameLabel": "nom du worktree", "LaunchHintMove": "déplacer", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} sessions enregistrées", "LaunchCreatingWorktree": "Création du worktree…", "LaunchWorktreeFailed": "Échec du worktree : {error}", + "LaunchWorktreeCreated": "Nouveau worktree {path} sur la branche {branch}", "LaunchNoSavedSessions": "Aucune session enregistrée pour ce workspace.", "LaunchComposerHint": "Entrée envoie · Maj+Entrée nouvelle ligne · Échap retour", "LaunchNoModelConnected": "aucun modèle connecté", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Accès aux identifiants externes révoqué pour {provider}", "ProviderExternalRevokeFailedToast": "L'accès aux identifiants externes n'a pas été révoqué : {error}", "ThemeSurfaceTitle": "thème · aperçu en direct", - "ThemeTreatmentDeepseaUnavailable": "Traitement Deepsea indisponible — l'arrière-plan appartient au Terminal", - "ThemeTreatmentFlatActive": "Traitement Flat — actif", - "ThemeTreatmentDeepseaActive": "Traitement Deepsea — actif", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "membres", "FleetRosterTabSetup": "setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Garde le raisonnement du modèle masqué ; réponses et outils restent visibles.", "ConfigChoiceDetailThinkingHighlightOn": "Remplit l'arrière-plan du raisonnement du modèle.", "ConfigChoiceDetailThinkingHighlightOff": "Garde le rail en pointillés et le texte en italique sans arrière-plan rempli.", - "ConfigChoiceDetailOceanDeepsea": "Utilise un seul champ de couleur océan continu.", - "ConfigChoiceDetailOceanFlat": "Utilise une seule couleur de fond unie.", "ConfigHintModel": "modèle de la route en direct de cette session ; Entrée ouvre /model", "ConfigHintFastModel": "utilisé par le routage Auto et par model_strength=faster des agents quand ce fournisseur a un modèle frère connu", "ConfigHintProvider": "fournisseur de la route en direct de cette session ; Entrée ouvre /provider (identifiants, modèle et point de terminaison changent ensemble)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "apaise le chrome de la transcription et le détail des outils ; indépendant du mouvement en direct", "ConfigHintLowMotion": "activé surcharge le mouvement de l'état en direct ; la sortie du modèle est inchangée", "ConfigHintFancyAnimations": "activé anime fidèlement l'état en direct des outils, du statut et de l'océan", - "ConfigHintOceanTreatment": "deepsea | flat (apparence ; indépendant du mouvement)", "ConfigHintShowThinking": "affiche ou masque le raisonnement du modèle dans le chat ; les listes de tâches restent concises", "ConfigHintThinkingDefaultExpanded": "déplie le raisonnement du modèle par défaut ; Espace bascule toujours chaque bloc", "ConfigHintThinkingPreviewLines": "lignes d'aperçu des pensées terminées repliées (défaut 2 ; 0=en-tête seul ; 10=ancien vidage)", diff --git a/crates/tui/locales/hi.json b/crates/tui/locales/hi.json index 75546f4af2..d7623fa8c0 100644 --- a/crates/tui/locales/hi.json +++ b/crates/tui/locales/hi.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "थीम", "ConfigLabelLocale": "भाषा", "ConfigLabelBackground": "पृष्ठभूमि", - "ConfigLabelOceanTreatment": "Ocean प्रभाव", "ConfigLabelWorkSurfacePlacement": "साइडबार स्थिति", "ConfigLabelTopHeight": "शीर्ष पट्टी ऊँचाई", "ConfigLabelSideWidth": "साइड पट्टी चौड़ाई", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "सलाहकार", "SubagentsRoleCustom": "कस्टम", "HelpUnknownCommand": "अज्ञात कमांड: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "मॉडल:", "HomeMode": "मोड:", "HomeWorkspace": "वर्कस्पेस:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "प्रोवाइडर सेट", "LaunchProviderSetupShort": "प्रोवाइडर सेटअप", "LaunchMenuChangelog": "चेंजलॉग", - "LaunchWorktreePrompt": "ब्रांच/worktree का नाम दें, या स्वचालित नाम के लिए Enter दबाएँ।", + "LaunchWorktreePrompt": "ब्रांच/worktree का नाम दें, या स्वचालित नाम के लिए Enter दबाएँ। Esc से वापस।", "LaunchWorktreeNeedsGit": "नए worktree के लिए Git रिपॉज़िटरी आवश्यक।", "LaunchWorktreeNameLabel": "worktree नाम", "LaunchHintMove": "घुमाएँ", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} सहेजे सत्र", "LaunchCreatingWorktree": "worktree बन रहा है…", "LaunchWorktreeFailed": "worktree विफल: {error}", + "LaunchWorktreeCreated": "नया worktree {path}, ब्रांच {branch} पर", "LaunchNoSavedSessions": "इस वर्कस्पेस के लिए कोई सहेजा सत्र नहीं।", "LaunchComposerHint": "Enter भेजें · Shift+Enter नई पंक्ति · Esc वापस", "LaunchNoModelConnected": "कोई मॉडल कनेक्ट नहीं", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "{provider} के लिए बाहरी क्रेडेंशल एक्सेस रद्द", "ProviderExternalRevokeFailedToast": "बाहरी क्रेडेंशल एक्सेस रद्द नहीं हुआ: {error}", "ThemeSurfaceTitle": "थीम · लाइव प्रीव्यू", - "ThemeTreatmentDeepseaUnavailable": "शैली Deepsea अनुपलब्ध — बैकग्राउंड Terminal के पास है", - "ThemeTreatmentFlatActive": "शैली Flat — सक्रिय", - "ThemeTreatmentDeepseaActive": "शैली Deepsea — सक्रिय", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "सदस्य", "FleetRosterTabSetup": "सेटअप", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "मॉडल का तर्क छिपा रखता है; उत्तर और टूल दिखते रहते हैं।", "ConfigChoiceDetailThinkingHighlightOn": "मॉडल तर्क की पृष्ठभूमि भरता है।", "ConfigChoiceDetailThinkingHighlightOff": "बिना भरी पृष्ठभूमि के बिंदीदार रेल और तिरछा पाठ रखता है।", - "ConfigChoiceDetailOceanDeepsea": "एक सतत समुद्री रंग क्षेत्र उपयोग करता है।", - "ConfigChoiceDetailOceanFlat": "एक ही सपाट पृष्ठभूमि रंग उपयोग करता है।", "ConfigHintModel": "इस सत्र के लाइव रूट का मॉडल; Enter से /model खुलता है", "ConfigHintFastModel": "जब इस प्रदाता का कोई ज्ञात सहोदर मॉडल हो तो Auto रूटिंग और एजेंट का model_strength=faster इसे उपयोग करता है", "ConfigHintProvider": "इस सत्र के लाइव रूट का प्रदाता; Enter से /provider खुलता है (क्रेडेंशियल, मॉडल और endpoint एक साथ बदलते हैं)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "ट्रांसक्रिप्ट की सजावट और टूल विवरण को शांत करता है; लाइव गति से स्वतंत्र", "ConfigHintLowMotion": "चालू होने पर लाइव स्थिति की गति अधिभावित होती है; मॉडल आउटपुट अपरिवर्तित रहता है", "ConfigHintFancyAnimations": "चालू होने पर टूल, स्थिति और समुद्र की लाइव स्थिति सच्चाई से एनिमेट होती है", - "ConfigHintOceanTreatment": "deepsea | flat (रूप; गति से स्वतंत्र)", "ConfigHintShowThinking": "चैट में मॉडल का तर्क दिखाएँ या छिपाएँ; कार्य सूचियाँ संक्षिप्त रहती हैं", "ConfigHintThinkingDefaultExpanded": "मॉडल का तर्क डिफ़ॉल्ट रूप से फैला रहे; Space से हर खंड अब भी टॉगल होता है", "ConfigHintThinkingPreviewLines": "समेटे गए पूर्ण विचार की पूर्वावलोकन पंक्तियाँ (डिफ़ॉल्ट 2; 0=केवल शीर्षक; 10=पुराना डंप)", diff --git a/crates/tui/locales/id.json b/crates/tui/locales/id.json index 532bb4d859..5a9747a1fa 100644 --- a/crates/tui/locales/id.json +++ b/crates/tui/locales/id.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Tema", "ConfigLabelLocale": "Bahasa", "ConfigLabelBackground": "Latar belakang", - "ConfigLabelOceanTreatment": "Efek Ocean", "ConfigLabelWorkSurfacePlacement": "Posisi bilah sisi", "ConfigLabelTopHeight": "Tinggi bilah atas", "ConfigLabelSideWidth": "Lebar bilah sisi", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "konsultan", "SubagentsRoleCustom": "kustom", "HelpUnknownCommand": "Perintah tidak dikenal: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Model:", "HomeMode": "Mode:", "HomeWorkspace": "Workspace:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "penyedia dikonfigurasi", "LaunchProviderSetupShort": "siapkan penyedia", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Beri nama branch/worktree, atau tekan Enter untuk nama otomatis.", + "LaunchWorktreePrompt": "Beri nama branch/worktree, atau tekan Enter untuk nama otomatis. Esc untuk kembali.", "LaunchWorktreeNeedsGit": "Worktree baru memerlukan repositori Git.", "LaunchWorktreeNameLabel": "nama worktree", "LaunchHintMove": "pindah", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} sesi tersimpan", "LaunchCreatingWorktree": "Membuat worktree…", "LaunchWorktreeFailed": "Worktree gagal: {error}", + "LaunchWorktreeCreated": "Worktree baru {path} di branch {branch}", "LaunchNoSavedSessions": "Tidak ada sesi tersimpan untuk workspace ini.", "LaunchComposerHint": "Enter kirim · Shift+Enter baris baru · Esc kembali", "LaunchNoModelConnected": "tidak ada model terhubung", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Akses kredensial eksternal dicabut untuk {provider}", "ProviderExternalRevokeFailedToast": "Akses kredensial eksternal tidak tercabut: {error}", "ThemeSurfaceTitle": "tema · pratinjau langsung", - "ThemeTreatmentDeepseaUnavailable": "Treatment Deepsea tidak tersedia — latar dikendalikan Terminal", - "ThemeTreatmentFlatActive": "Treatment Flat — aktif", - "ThemeTreatmentDeepseaActive": "Treatment Deepsea — aktif", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "anggota", "FleetRosterTabSetup": "setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Menyembunyikan penalaran model; jawaban dan alat tetap terlihat.", "ConfigChoiceDetailThinkingHighlightOn": "Mengisi latar penalaran model.", "ConfigChoiceDetailThinkingHighlightOff": "Mempertahankan rel putus-putus dan teks miring tanpa latar terisi.", - "ConfigChoiceDetailOceanDeepsea": "Memakai satu bidang warna lautan yang kontinu.", - "ConfigChoiceDetailOceanFlat": "Memakai satu warna latar datar.", "ConfigHintModel": "model rute langsung untuk sesi ini; Enter membuka /model", "ConfigHintFastModel": "dipakai oleh perutean Auto dan model_strength=faster agen saat penyedia ini punya model saudara yang dikenal", "ConfigHintProvider": "penyedia rute langsung untuk sesi ini; Enter membuka /provider (kredensial, model, dan endpoint berganti bersama)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "menenangkan hiasan transkrip dan detail alat; tidak bergantung pada gerakan langsung", "ConfigHintLowMotion": "aktif menimpa gerakan status langsung; keluaran model tidak berubah", "ConfigHintFancyAnimations": "aktif menganimasikan status langsung alat, status, dan lautan secara jujur", - "ConfigHintOceanTreatment": "deepsea | flat (tampilan; tidak bergantung pada gerakan)", "ConfigHintShowThinking": "menampilkan atau menyembunyikan penalaran model di obrolan; daftar tugas tetap ringkas", "ConfigHintThinkingDefaultExpanded": "membentangkan penalaran model secara bawaan; Space tetap mengubah tiap blok", "ConfigHintThinkingPreviewLines": "baris pratinjau pemikiran selesai yang dilipat (bawaan 2; 0=hanya judul; 10=dump lama)", diff --git a/crates/tui/locales/ja.json b/crates/tui/locales/ja.json index 9c34e0ad74..911e639475 100644 --- a/crates/tui/locales/ja.json +++ b/crates/tui/locales/ja.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "テーマ", "ConfigLabelLocale": "言語", "ConfigLabelBackground": "背景", - "ConfigLabelOceanTreatment": "海洋表現", "ConfigLabelWorkSurfacePlacement": "サイドバー位置", "ConfigLabelTopHeight": "トップバーの高さ", "ConfigLabelSideWidth": "サイドバーの幅", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "コンサルタント", "SubagentsRoleCustom": "カスタム", "HelpUnknownCommand": "不明なコマンド: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "モデル:", "HomeMode": "モード:", "HomeWorkspace": "ワークスペース:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "プロバイダー設定済み", "LaunchProviderSetupShort": "プロバイダー設定", "LaunchMenuChangelog": "更新履歴", - "LaunchWorktreePrompt": "ブランチ/ワークツリーに名前を付けるか、Enter を押して自動命名にします。", + "LaunchWorktreePrompt": "ブランチ/ワークツリーに名前を付けるか、Enter を押して自動命名にします。 Esc で戻ります。", "LaunchWorktreeNeedsGit": "新規ワークツリーには Git リポジトリが必要です。", "LaunchWorktreeNameLabel": "ワークツリー名", "LaunchHintMove": "移動", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "保存済みセッション {count} 件", "LaunchCreatingWorktree": "ワークツリーを作成中…", "LaunchWorktreeFailed": "ワークツリーの作成に失敗: {error}", + "LaunchWorktreeCreated": "新しいワークツリー {path}(ブランチ {branch})", "LaunchNoSavedSessions": "このワークスペースには保存済みセッションがありません。", "LaunchComposerHint": "Enter 送信 · Shift+Enter 改行 · Esc 戻る", "LaunchNoModelConnected": "モデル未接続", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "{provider} の外部認証情報へのアクセスを取り消しました", "ProviderExternalRevokeFailedToast": "外部認証情報へのアクセスを取り消せませんでした: {error}", "ThemeSurfaceTitle": "テーマ · ライブプレビュー", - "ThemeTreatmentDeepseaUnavailable": "スタイル Deepsea は利用不可 — 背景はターミナル側が管理しています", - "ThemeTreatmentFlatActive": "スタイル Flat — 有効", - "ThemeTreatmentDeepseaActive": "スタイル Deepsea — 有効", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "メンバー", "FleetRosterTabSetup": "セットアップ", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "モデルの推論を隠します。回答とツールは表示されたままです。", "ConfigChoiceDetailThinkingHighlightOn": "モデル推論の背景を塗りつぶします。", "ConfigChoiceDetailThinkingHighlightOff": "背景を塗らず、破線のレールと斜体テキストを保ちます。", - "ConfigChoiceDetailOceanDeepsea": "連続した一つの海の色面を使います。", - "ConfigChoiceDetailOceanFlat": "単一のフラットな背景色を使います。", "ConfigHintModel": "このセッションのライブルートのモデル。Enter で /model を開きます", "ConfigHintFastModel": "このプロバイダーに既知の兄弟モデルがあるとき、Auto ルーティングとエージェントの model_strength=faster が使います", "ConfigHintProvider": "このセッションのライブルートのプロバイダー。Enter で /provider を開きます (資格情報・モデル・エンドポイントを一緒に切り替え)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "トランスクリプトの装飾とツール詳細を静かにします。ライブモーションとは独立です", "ConfigHintLowMotion": "オンでライブ状態の動きを上書きします。モデル出力は変わりません", "ConfigHintFancyAnimations": "オンでツール・状態・海のライブ状態を忠実にアニメーションします", - "ConfigHintOceanTreatment": "deepsea | flat (外観。モーションとは独立)", "ConfigHintShowThinking": "チャット内のモデル推論の表示/非表示。タスク一覧は簡潔なままです", "ConfigHintThinkingDefaultExpanded": "モデル推論を既定で展開します。Space で各ブロックを切り替えられます", "ConfigHintThinkingPreviewLines": "折りたたんだ完了済み思考のプレビュー行数 (既定 2、0=見出しのみ、10=古いダンプ)", diff --git a/crates/tui/locales/ko.json b/crates/tui/locales/ko.json index b2f9a9cc8a..5e3a6a8486 100644 --- a/crates/tui/locales/ko.json +++ b/crates/tui/locales/ko.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "테마", "ConfigLabelLocale": "언어", "ConfigLabelBackground": "배경", - "ConfigLabelOceanTreatment": "바다 효과", "ConfigLabelWorkSurfacePlacement": "사이드바 위치", "ConfigLabelTopHeight": "상단 바 높이", "ConfigLabelSideWidth": "사이드바 너비", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "컨설턴트", "SubagentsRoleCustom": "사용자 지정", "HelpUnknownCommand": "알 수 없는 명령어: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "모델:", "HomeMode": "모드:", "HomeWorkspace": "작업 공간:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "공급자 설정됨", "LaunchProviderSetupShort": "공급자 설정", "LaunchMenuChangelog": "변경 이력", - "LaunchWorktreePrompt": "브랜치/워크트리 이름을 입력하거나, Enter를 누르면 자동으로 이름이 지정됩니다.", + "LaunchWorktreePrompt": "브랜치/워크트리 이름을 입력하거나, Enter를 누르면 자동으로 이름이 지정됩니다. Esc로 돌아갑니다.", "LaunchWorktreeNeedsGit": "새 워크트리는 Git 저장소가 필요합니다.", "LaunchWorktreeNameLabel": "워크트리 이름", "LaunchHintMove": "이동", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "저장된 세션 {count}개", "LaunchCreatingWorktree": "워크트리 생성 중…", "LaunchWorktreeFailed": "워크트리 생성 실패: {error}", + "LaunchWorktreeCreated": "새 워크트리 {path} (브랜치 {branch})", "LaunchNoSavedSessions": "이 작업 공간에는 저장된 세션이 없습니다.", "LaunchComposerHint": "Enter 전송 · Shift+Enter 새 줄 · Esc 뒤로", "LaunchNoModelConnected": "연결된 모델 없음", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "{provider}의 외부 자격 증명 접근을 취소했습니다", "ProviderExternalRevokeFailedToast": "외부 자격 증명 접근을 취소하지 못했습니다: {error}", "ThemeSurfaceTitle": "테마 · 실시간 미리보기", - "ThemeTreatmentDeepseaUnavailable": "표현 방식 Deepsea 사용 불가 — 배경은 터미널이 제어합니다", - "ThemeTreatmentFlatActive": "표현 방식 Flat — 활성", - "ThemeTreatmentDeepseaActive": "표현 방식 Deepsea — 활성", "FleetRosterHeaderLabel": "Pod", "FleetRosterTabRoster": "멤버", "FleetRosterTabSetup": "설정", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "모델 추론을 숨깁니다. 답변과 도구는 계속 보입니다.", "ConfigChoiceDetailThinkingHighlightOn": "모델 추론 배경을 채웁니다.", "ConfigChoiceDetailThinkingHighlightOff": "배경을 채우지 않고 점선 추론 레일과 기울임 텍스트를 유지합니다.", - "ConfigChoiceDetailOceanDeepsea": "하나의 연속된 바다 색 면을 사용합니다.", - "ConfigChoiceDetailOceanFlat": "단일 평면 배경색을 사용합니다.", "ConfigHintModel": "이 세션의 실시간 경로 모델. Enter로 /model 열기", "ConfigHintFastModel": "이 제공자에 알려진 형제 모델이 있을 때 Auto 라우팅과 에이전트의 model_strength=faster가 사용", "ConfigHintProvider": "이 세션의 실시간 경로 제공자. Enter로 /provider 열기 (자격 증명, 모델, 엔드포인트가 함께 전환)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "대화 기록 장식과 도구 세부 정보를 조용하게. 실시간 모션과 무관", "ConfigHintLowMotion": "켜면 실시간 상태 모션을 재정의. 모델 출력은 변하지 않음", "ConfigHintFancyAnimations": "켜면 도구, 상태, 바다의 실시간 상태를 사실대로 애니메이션", - "ConfigHintOceanTreatment": "deepsea | flat (모양. 모션과 무관)", "ConfigHintShowThinking": "채팅에서 모델 추론 표시 또는 숨김. 작업 목록은 간결하게 유지", "ConfigHintThinkingDefaultExpanded": "모델 추론을 기본으로 펼침. Space로 각 블록 전환 가능", "ConfigHintThinkingPreviewLines": "접힌 완료 사고의 미리보기 행 수 (기본 2, 0=헤더만, 10=이전 덤프)", diff --git a/crates/tui/locales/pt-BR.json b/crates/tui/locales/pt-BR.json index f3aa2e3a1e..8cbae7ad74 100644 --- a/crates/tui/locales/pt-BR.json +++ b/crates/tui/locales/pt-BR.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Tema", "ConfigLabelLocale": "Idioma", "ConfigLabelBackground": "Plano de fundo", - "ConfigLabelOceanTreatment": "Tratamento oceânico", "ConfigLabelWorkSurfacePlacement": "Posição da barra lateral", "ConfigLabelTopHeight": "Altura da barra superior", "ConfigLabelSideWidth": "Largura da barra lateral", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "consultor", "SubagentsRoleCustom": "personalizado", "HelpUnknownCommand": "Comando desconhecido: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Modelo:", "HomeMode": "Modo:", "HomeWorkspace": "Espaço de trabalho:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "provedor configurado", "LaunchProviderSetupShort": "configurar provedor", "LaunchMenuChangelog": "Changelog", - "LaunchWorktreePrompt": "Nomeie a branch/worktree ou pressione Enter para um nome automático.", + "LaunchWorktreePrompt": "Nomeie a branch/worktree ou pressione Enter para um nome automático. Esc para voltar.", "LaunchWorktreeNeedsGit": "Novo worktree requer um repositório Git.", "LaunchWorktreeNameLabel": "nome do worktree", "LaunchHintMove": "mover", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "{count} sessões salvas", "LaunchCreatingWorktree": "Criando worktree…", "LaunchWorktreeFailed": "Falha no worktree: {error}", + "LaunchWorktreeCreated": "Novo worktree {path} na branch {branch}", "LaunchNoSavedSessions": "Nenhuma sessão salva para este workspace.", "LaunchComposerHint": "Enter envia · Shift+Enter nova linha · Esc volta", "LaunchNoModelConnected": "nenhum modelo conectado", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "Acesso à credencial externa revogado para {provider}", "ProviderExternalRevokeFailedToast": "O acesso à credencial externa não foi revogado: {error}", "ThemeSurfaceTitle": "tema · prévia ao vivo", - "ThemeTreatmentDeepseaUnavailable": "Tratamento Deepsea indisponível — o fundo pertence ao Terminal", - "ThemeTreatmentFlatActive": "Tratamento Flat — ativo", - "ThemeTreatmentDeepseaActive": "Tratamento Deepsea — ativo", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "membros", "FleetRosterTabSetup": "setup", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Mantém o raciocínio do modelo oculto; respostas e ferramentas continuam visíveis.", "ConfigChoiceDetailThinkingHighlightOn": "Preenche o fundo do raciocínio do modelo.", "ConfigChoiceDetailThinkingHighlightOff": "Mantém o trilho tracejado e o texto em itálico sem fundo preenchido.", - "ConfigChoiceDetailOceanDeepsea": "Usa um único campo de cor contínuo do oceano.", - "ConfigChoiceDetailOceanFlat": "Usa uma única cor de fundo plana.", "ConfigHintModel": "modelo da rota ao vivo desta sessão; Enter abre /model", "ConfigHintFastModel": "usado pelo roteamento Auto e por model_strength=faster dos agentes quando este provedor tem um irmão conhecido", "ConfigHintProvider": "provedor da rota ao vivo desta sessão; Enter abre /provider (credencial, modelo e endpoint mudam juntos)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "silencia o cromo da transcrição e o detalhe das ferramentas; independente do movimento ao vivo", "ConfigHintLowMotion": "ligado sobrepõe o movimento do estado ao vivo; a saída do modelo não muda", "ConfigHintFancyAnimations": "ligado anima com fidelidade o estado ao vivo de ferramentas, status e oceano", - "ConfigHintOceanTreatment": "deepsea | flat (aparência; independente do movimento)", "ConfigHintShowThinking": "mostra ou oculta o raciocínio do modelo no chat; as listas de tarefas continuam concisas", "ConfigHintThinkingDefaultExpanded": "expande o raciocínio do modelo por padrão; Espaço ainda alterna cada bloco", "ConfigHintThinkingPreviewLines": "linhas de prévia do pensamento concluído recolhido (padrão 2; 0=só cabeçalho; 10=despejo antigo)", diff --git a/crates/tui/locales/ru.json b/crates/tui/locales/ru.json index 8963869513..502297d054 100644 --- a/crates/tui/locales/ru.json +++ b/crates/tui/locales/ru.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Тема", "ConfigLabelLocale": "Язык", "ConfigLabelBackground": "Фон", - "ConfigLabelOceanTreatment": "Оформление Ocean", "ConfigLabelWorkSurfacePlacement": "Положение боковой панели", "ConfigLabelTopHeight": "Высота верхней панели", "ConfigLabelSideWidth": "Ширина боковой панели", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "консультант", "SubagentsRoleCustom": "пользовательский", "HelpUnknownCommand": "Неизвестная команда: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Модель:", "HomeMode": "Режим:", "HomeWorkspace": "Рабочая область:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "провайдер настроен", "LaunchProviderSetupShort": "настройка провайдера", "LaunchMenuChangelog": "Список изменений", - "LaunchWorktreePrompt": "Назовите ветку/worktree или нажмите Enter для автоматического имени.", + "LaunchWorktreePrompt": "Назовите ветку/worktree или нажмите Enter для автоматического имени. Esc — назад.", "LaunchWorktreeNeedsGit": "Новый worktree требует репозиторий Git.", "LaunchWorktreeNameLabel": "имя worktree", "LaunchHintMove": "перемещение", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "сохранённых сессий: {count}", "LaunchCreatingWorktree": "Создание worktree…", "LaunchWorktreeFailed": "Ошибка worktree: {error}", + "LaunchWorktreeCreated": "Новый worktree {path} в ветке {branch}", "LaunchNoSavedSessions": "Нет сохранённых сессий для этой рабочей области.", "LaunchComposerHint": "Enter — отправить · Shift+Enter — новая строка · Esc — назад", "LaunchNoModelConnected": "модель не подключена", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Доступ к внешним учётным данным отозван для {provider}", "ProviderExternalRevokeFailedToast": "Доступ к внешним учётным данным не отозван: {error}", "ThemeSurfaceTitle": "тема · живой предпросмотр", - "ThemeTreatmentDeepseaUnavailable": "Обработка Deepsea недоступна — фоном управляет терминал", - "ThemeTreatmentFlatActive": "Обработка Flat — активна", - "ThemeTreatmentDeepseaActive": "Обработка Deepsea — активна", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "участники", "FleetRosterTabSetup": "настройка", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Скрывает рассуждения модели; ответы и инструменты остаются видны.", "ConfigChoiceDetailThinkingHighlightOn": "Заливает фон рассуждений модели.", "ConfigChoiceDetailThinkingHighlightOff": "Сохраняет пунктирную линию и курсив без заливки фона.", - "ConfigChoiceDetailOceanDeepsea": "Использует одно непрерывное цветовое поле океана.", - "ConfigChoiceDetailOceanFlat": "Использует один плоский цвет фона.", "ConfigHintModel": "модель живого маршрута этой сессии; Enter открывает /model", "ConfigHintFastModel": "используется маршрутизацией Auto и model_strength=faster агентов, когда у провайдера есть известная родственная модель", "ConfigHintProvider": "провайдер живого маршрута этой сессии; Enter открывает /provider (учётные данные, модель и адрес переключаются вместе)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "приглушает оформление стенограммы и подробности инструментов; не зависит от живого движения", "ConfigHintLowMotion": "вкл переопределяет движение живого состояния; вывод модели не меняется", "ConfigHintFancyAnimations": "вкл честно анимирует живое состояние инструментов, статуса и океана", - "ConfigHintOceanTreatment": "deepsea | flat (оформление; не зависит от движения)", "ConfigHintShowThinking": "показывать или скрывать рассуждения модели в чате; списки задач остаются краткими", "ConfigHintThinkingDefaultExpanded": "раскрывать рассуждения модели по умолчанию; Space по-прежнему переключает каждый блок", "ConfigHintThinkingPreviewLines": "строки предпросмотра свёрнутой завершённой мысли (по умолчанию 2; 0=только заголовок; 10=старый вывод)", diff --git a/crates/tui/locales/uk.json b/crates/tui/locales/uk.json index 9d29fb084c..7e0c7d08c9 100644 --- a/crates/tui/locales/uk.json +++ b/crates/tui/locales/uk.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Тема", "ConfigLabelLocale": "Мова", "ConfigLabelBackground": "Тло", - "ConfigLabelOceanTreatment": "Оформлення Ocean", "ConfigLabelWorkSurfacePlacement": "Позиція бічної панелі", "ConfigLabelTopHeight": "Висота верхньої панелі", "ConfigLabelSideWidth": "Ширина бічної панелі", @@ -712,7 +711,7 @@ "SubagentsRoleConsultant": "консультант", "SubagentsRoleCustom": "користувацький", "HelpUnknownCommand": "Невідома команда: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Модель:", "HomeMode": "Режим:", "HomeWorkspace": "Робоча область:", @@ -1228,7 +1227,7 @@ "LaunchProviderConfiguredShort": "провайдер налаштований", "LaunchProviderSetupShort": "налаштувати провайдера", "LaunchMenuChangelog": "Журнал змін", - "LaunchWorktreePrompt": "Назвіть гілку/worktree або натисніть Enter для автоматичної назви.", + "LaunchWorktreePrompt": "Назвіть гілку/worktree або натисніть Enter для автоматичної назви. Esc — назад.", "LaunchWorktreeNeedsGit": "Новий worktree потребує репозиторію Git.", "LaunchWorktreeNameLabel": "назва worktree", "LaunchHintMove": "рухатися", @@ -1238,6 +1237,7 @@ "LaunchSavedSessionsPlural": "{count} збережених сесій", "LaunchCreatingWorktree": "Створення worktree…", "LaunchWorktreeFailed": "Worktree не вдався: {error}", + "LaunchWorktreeCreated": "Новий worktree {path} у гілці {branch}", "LaunchNoSavedSessions": "Немає збережених сесій для цього робочого простору.", "LaunchComposerHint": "Enter — надіслати · Shift+Enter — новий рядок · Esc — назад", "LaunchNoModelConnected": "модель не підключено", @@ -1438,9 +1438,6 @@ "ProviderExternalRevokedToast": "Доступ до зовнішніх облікових даних відкликано для {provider}", "ProviderExternalRevokeFailedToast": "Доступ до зовнішніх облікових даних не відкликано: {error}", "ThemeSurfaceTitle": "тема · живий перегляд", - "ThemeTreatmentDeepseaUnavailable": "Обробка Deepsea недоступна — тлом керує термінал", - "ThemeTreatmentFlatActive": "Обробка Flat — активна", - "ThemeTreatmentDeepseaActive": "Обробка Deepsea — активна", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "учасники", "FleetRosterTabSetup": "налаштування", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Ховає міркування моделі; відповіді та інструменти залишаються видимими.", "ConfigChoiceDetailThinkingHighlightOn": "Заливає тло міркувань моделі.", "ConfigChoiceDetailThinkingHighlightOff": "Зберігає пунктирну лінію та курсив без заливки тла.", - "ConfigChoiceDetailOceanDeepsea": "Використовує одне неперервне кольорове поле океану.", - "ConfigChoiceDetailOceanFlat": "Використовує один плаский колір тла.", "ConfigHintModel": "модель живого маршруту цього сеансу; Enter відкриває /model", "ConfigHintFastModel": "використовується маршрутизацією Auto та model_strength=faster агентів, коли провайдер має відому споріднену модель", "ConfigHintProvider": "провайдер живого маршруту цього сеансу; Enter відкриває /provider (облікові дані, модель і адреса перемикаються разом)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "приглушує оформлення стенограми та подробиці інструментів; не залежить від живого руху", "ConfigHintLowMotion": "увімк перевизначає рух живого стану; вивід моделі не змінюється", "ConfigHintFancyAnimations": "увімк чесно анімує живий стан інструментів, статусу та океану", - "ConfigHintOceanTreatment": "deepsea | flat (оформлення; не залежить від руху)", "ConfigHintShowThinking": "показувати або ховати міркування моделі в чаті; списки завдань залишаються стислими", "ConfigHintThinkingDefaultExpanded": "розгортати міркування моделі типово; Space і далі перемикає кожен блок", "ConfigHintThinkingPreviewLines": "рядки попереднього перегляду згорнутої завершеної думки (типово 2; 0=лише заголовок; 10=старий вивід)", diff --git a/crates/tui/locales/vi.json b/crates/tui/locales/vi.json index a3fb2c92ed..3097903acb 100644 --- a/crates/tui/locales/vi.json +++ b/crates/tui/locales/vi.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "Chủ đề", "ConfigLabelLocale": "Ngôn ngữ", "ConfigLabelBackground": "Nền", - "ConfigLabelOceanTreatment": "Hiệu ứng đại dương", "ConfigLabelWorkSurfacePlacement": "Vị trí thanh bên", "ConfigLabelTopHeight": "Chiều cao thanh trên", "ConfigLabelSideWidth": "Chiều rộng thanh bên", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "tư vấn", "SubagentsRoleCustom": "tùy chỉnh", "HelpUnknownCommand": "Lệnh không xác định: {topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "Mô hình:", "HomeMode": "Chế độ:", "HomeWorkspace": "Không gian làm việc:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "đã đặt nhà cung cấp", "LaunchProviderSetupShort": "thiết lập nhà cung cấp", "LaunchMenuChangelog": "Nhật ký thay đổi", - "LaunchWorktreePrompt": "Đặt tên cho nhánh/worktree, hoặc nhấn Enter để dùng tên tự động.", + "LaunchWorktreePrompt": "Đặt tên cho nhánh/worktree, hoặc nhấn Enter để dùng tên tự động. Esc để quay lại.", "LaunchWorktreeNeedsGit": "Worktree mới yêu cầu một kho Git.", "LaunchWorktreeNameLabel": "tên worktree", "LaunchHintMove": "di chuyển", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "{count} phiên đã lưu", "LaunchCreatingWorktree": "Đang tạo worktree…", "LaunchWorktreeFailed": "Worktree thất bại: {error}", + "LaunchWorktreeCreated": "Worktree mới {path} trên nhánh {branch}", "LaunchNoSavedSessions": "Không có phiên đã lưu cho workspace này.", "LaunchComposerHint": "Enter gửi · Shift+Enter dòng mới · Esc quay lại", "LaunchNoModelConnected": "chưa kết nối mô hình", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "Đã thu hồi quyền truy cập thông tin xác thực ngoài cho {provider}", "ProviderExternalRevokeFailedToast": "Không thể thu hồi quyền truy cập thông tin xác thực ngoài: {error}", "ThemeSurfaceTitle": "giao diện · xem trước trực tiếp", - "ThemeTreatmentDeepseaUnavailable": "Hiệu ứng Deepsea không khả dụng — terminal kiểm soát nền", - "ThemeTreatmentFlatActive": "Hiệu ứng Flat — đang hoạt động", - "ThemeTreatmentDeepseaActive": "Hiệu ứng Deepsea — đang hoạt động", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "thành viên", "FleetRosterTabSetup": "thiết lập", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "Ẩn suy luận của mô hình; câu trả lời và công cụ vẫn hiển thị.", "ConfigChoiceDetailThinkingHighlightOn": "Tô nền cho suy luận của mô hình.", "ConfigChoiceDetailThinkingHighlightOff": "Giữ thanh ray nét đứt và chữ nghiêng mà không tô nền.", - "ConfigChoiceDetailOceanDeepsea": "Dùng một trường màu đại dương liên tục.", - "ConfigChoiceDetailOceanFlat": "Dùng một màu nền phẳng duy nhất.", "ConfigHintModel": "mô hình của tuyến trực tiếp trong phiên này; Enter mở /model", "ConfigHintFastModel": "được định tuyến Auto và model_strength=faster của tác nhân dùng khi nhà cung cấp này có mô hình anh em đã biết", "ConfigHintProvider": "nhà cung cấp của tuyến trực tiếp trong phiên này; Enter mở /provider (thông tin xác thực, mô hình và endpoint chuyển cùng nhau)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "làm dịu khung bản ghi và chi tiết công cụ; độc lập với chuyển động trực tiếp", "ConfigHintLowMotion": "bật sẽ ghi đè chuyển động của trạng thái trực tiếp; đầu ra của mô hình không đổi", "ConfigHintFancyAnimations": "bật sẽ hoạt hóa trung thực trạng thái trực tiếp của công cụ, trạng thái và đại dương", - "ConfigHintOceanTreatment": "deepsea | flat (giao diện; độc lập với chuyển động)", "ConfigHintShowThinking": "hiện hoặc ẩn suy luận của mô hình trong trò chuyện; danh sách tác vụ vẫn gọn", "ConfigHintThinkingDefaultExpanded": "mặc định mở rộng suy luận của mô hình; Space vẫn bật/tắt từng khối", "ConfigHintThinkingPreviewLines": "số hàng xem trước của suy nghĩ đã hoàn thành khi thu gọn (mặc định 2; 0=chỉ tiêu đề; 10=dạng cũ)", diff --git a/crates/tui/locales/zh-Hans.json b/crates/tui/locales/zh-Hans.json index 167ea863f8..b95a251f7a 100644 --- a/crates/tui/locales/zh-Hans.json +++ b/crates/tui/locales/zh-Hans.json @@ -263,7 +263,6 @@ "ConfigLabelTheme": "主题", "ConfigLabelLocale": "语言", "ConfigLabelBackground": "背景", - "ConfigLabelOceanTreatment": "海洋效果", "ConfigLabelWorkSurfacePlacement": "侧栏位置", "ConfigLabelTopHeight": "顶部栏高度", "ConfigLabelSideWidth": "侧栏宽度", @@ -729,7 +728,7 @@ "SubagentsRoleConsultant": "顾问", "SubagentsRoleCustom": "自定义", "HelpUnknownCommand": "未知命令:{topic}", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeModel": "模型:", "HomeMode": "模式:", "HomeWorkspace": "工作区:", @@ -1251,7 +1250,7 @@ "LaunchProviderConfiguredShort": "提供商已配置", "LaunchProviderSetupShort": "设置提供商", "LaunchMenuChangelog": "更新日志", - "LaunchWorktreePrompt": "为分支/工作树命名,或按 Enter 使用自动名称。", + "LaunchWorktreePrompt": "为分支/工作树命名,或按 Enter 使用自动名称。 Esc 返回。", "LaunchWorktreeNeedsGit": "新建工作树需要 Git 仓库。", "LaunchWorktreeNameLabel": "工作树名称", "LaunchHintMove": "移动", @@ -1261,6 +1260,7 @@ "LaunchSavedSessionsPlural": "已保存 {count} 个会话", "LaunchCreatingWorktree": "正在创建工作树…", "LaunchWorktreeFailed": "工作树创建失败:{error}", + "LaunchWorktreeCreated": "新工作树 {path},分支 {branch}", "LaunchNoSavedSessions": "此工作区没有已保存的会话。", "LaunchComposerHint": "Enter 发送 · Shift+Enter 换行 · Esc 返回", "LaunchNoModelConnected": "未连接模型", @@ -1461,9 +1461,6 @@ "ProviderExternalRevokedToast": "已撤销{provider}的外部凭据访问权限", "ProviderExternalRevokeFailedToast": "未能撤销外部凭据访问权限:{error}", "ThemeSurfaceTitle": "主题 · 实时预览", - "ThemeTreatmentDeepseaUnavailable": "效果 Deepsea 不可用 — 背景由终端接管", - "ThemeTreatmentFlatActive": "效果 Flat — 已启用", - "ThemeTreatmentDeepseaActive": "效果 Deepsea — 已启用", "FleetRosterHeaderLabel": "pod", "FleetRosterTabRoster": "成员", "FleetRosterTabSetup": "设置", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "隐藏模型推理;回答和工具仍然可见。", "ConfigChoiceDetailThinkingHighlightOn": "填充模型推理的背景。", "ConfigChoiceDetailThinkingHighlightOff": "保留虚线推理边栏和斜体文字,不填充背景。", - "ConfigChoiceDetailOceanDeepsea": "使用一个连续的海洋色场。", - "ConfigChoiceDetailOceanFlat": "使用单一的平面背景色。", "ConfigHintModel": "本会话实时路由的模型;按 Enter 打开 /model", "ConfigHintFastModel": "当提供商有已知的同系模型时,供 Auto 路由和代理的 model_strength=faster 使用", "ConfigHintProvider": "本会话实时路由的提供商;按 Enter 打开 /provider(凭据、模型和端点一起切换)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "让对话记录装饰和工具详情更安静;与实时动效无关", "ConfigHintLowMotion": "开启后覆盖实时状态动效;模型输出不变", "ConfigHintFancyAnimations": "开启后如实为工具、状态和海洋的实时状态添加动画", - "ConfigHintOceanTreatment": "deepsea | flat(外观;与动效无关)", "ConfigHintShowThinking": "在聊天中显示或隐藏模型推理;任务列表保持简洁", "ConfigHintThinkingDefaultExpanded": "默认展开模型推理;Space 仍可切换每个块", "ConfigHintThinkingPreviewLines": "折叠的已完成思考预览行数(默认 2;0=仅标题;10=旧式转储)", diff --git a/crates/tui/locales/zh-Hant.json b/crates/tui/locales/zh-Hant.json index d9ff2e5768..dedeb43bb0 100644 --- a/crates/tui/locales/zh-Hant.json +++ b/crates/tui/locales/zh-Hant.json @@ -574,7 +574,6 @@ "ConfigLabelMentionMenuLimit": "提及選單上限", "ConfigLabelMentionWalkDepth": "檔案提及深度", "ConfigLabelModel": "目前提供商模型", - "ConfigLabelOceanTreatment": "海洋效果", "ConfigLabelPasteBurstDetection": "粘貼檢測", "ConfigLabelPermissionPosture": "新工作階段權限", "ConfigLabelProvider": "目前提供商", @@ -953,7 +952,7 @@ "HomeAgentModeReviewTip": " 用 /mode plan 來先調查搜尋與生成計畫", "HomeAgentModeTip": "Act — 在目前工作階段中用工具直接工作", "HomeAgentModeYoloTip": " Shift+Tab 循環權限: Ask → Auto-Review → Full Access", - "HomeDashboardTitle": "Codewhale", + "HomeDashboardTitle": "codewhale", "HomeGoalModeTip": "Goal 跟蹤 - 設定 /goal <目標> 以跟蹤持久目標", "HomeHistory": "歷史:", "HomeMode": "模式:", @@ -1115,9 +1114,10 @@ "LaunchSavedSessionsPlural": "已儲存 {count} 個工作階段", "LaunchTipFlags": "提示:-w 開啟工作區;-r 直接還原", "LaunchWorktreeFailed": "工作樹建立失敗:{error}", + "LaunchWorktreeCreated": "新工作樹 {path},分支 {branch}", "LaunchWorktreeNameLabel": "工作樹名稱", "LaunchWorktreeNeedsGit": "新建工作樹需要 Git 儲存庫。", - "LaunchWorktreePrompt": "為分支/工作樹命名,或按 Enter 使用自動名稱。", + "LaunchWorktreePrompt": "為分支/工作樹命名,或按 Enter 使用自動名稱。 Esc 返回。", "LinksCommunity": "社群與貢獻:", "LinksDashboard": "控制台:", "LinksDocs": "文件:", @@ -1630,9 +1630,6 @@ "SubagentsRoleConsultant": "顧問", "SubagentsRoleCustom": "自訂", "ThemeSurfaceTitle": "主題 · 實時預覽", - "ThemeTreatmentFlatActive": "效果 Flat — 已啟用", - "ThemeTreatmentDeepseaActive": "效果 Deepsea — 已啟用", - "ThemeTreatmentDeepseaUnavailable": "效果 Deepsea 無法使用 — 背景由終端接管", "ThinkingControlledByAutoRouting": "思考由自動模型路由控制;請先選擇固定模型。", "ToolFamilyDelegate": "代理", "ToolFamilyFanout": "扇出", @@ -1964,8 +1961,6 @@ "ConfigChoiceDetailShowThinkingOff": "隱藏模型推理;回答和工具仍然可見。", "ConfigChoiceDetailThinkingHighlightOn": "填滿模型推理的背景。", "ConfigChoiceDetailThinkingHighlightOff": "保留虛線推理側欄和斜體文字,不填滿背景。", - "ConfigChoiceDetailOceanDeepsea": "使用一個連續的海洋色場。", - "ConfigChoiceDetailOceanFlat": "使用單一的平面背景色。", "ConfigHintModel": "本工作階段即時路由的模型;按 Enter 開啟 /model", "ConfigHintFastModel": "當提供者有已知的同系模型時,供 Auto 路由和代理的 model_strength=faster 使用", "ConfigHintProvider": "本工作階段即時路由的提供者;按 Enter 開啟 /provider(憑證、模型和端點一起切換)", @@ -1992,7 +1987,6 @@ "ConfigHintCalmMode": "讓對話記錄裝飾和工具詳情更安靜;與即時動態效果無關", "ConfigHintLowMotion": "開啟後覆寫即時狀態動態效果;模型輸出不變", "ConfigHintFancyAnimations": "開啟後如實為工具、狀態和海洋的即時狀態加上動畫", - "ConfigHintOceanTreatment": "deepsea | flat(外觀;與動態效果無關)", "ConfigHintShowThinking": "在聊天中顯示或隱藏模型推理;任務清單保持簡潔", "ConfigHintThinkingDefaultExpanded": "預設展開模型推理;Space 仍可切換每個區塊", "ConfigHintThinkingPreviewLines": "摺疊的已完成思考預覽列數(預設 2;0=僅標題;10=舊式傾印)", diff --git a/crates/tui/src/commands/groups/config/config.rs b/crates/tui/src/commands/groups/config/config.rs index aabf1caad6..d770bc2e1a 100644 --- a/crates/tui/src/commands/groups/config/config.rs +++ b/crates/tui/src/commands/groups/config/config.rs @@ -1939,72 +1939,6 @@ fn live_route_setting_subject(key: &str) -> Option { } } -/// Apply the canonical theme picker's compound theme + ocean-treatment state. -/// -/// Preview and rollback update both live fields through the same per-setting -/// owner used by `/config`. A save validates the pair before any live mutation, -/// then persists both fields inside one [`Settings::transact`] critical section -/// so Deepsea cannot survive on disk with only half of its selection. -pub fn set_theme_selection( - app: &mut App, - theme: &str, - ocean_treatment: &str, - persist: bool, -) -> CommandResult { - let mut candidate = match Settings::load_persisted() { - Ok(settings) => settings, - Err(error) if !persist => { - app.status_message = Some(format!( - "Settings unavailable; applying session-only theme override ({error})" - )); - Settings::default() - } - Err(error) => return CommandResult::error(format!("Failed to load settings: {error}")), - }; - if let Err(error) = candidate.set("theme", theme) { - return CommandResult::error(error.to_string()); - } - if let Err(error) = candidate.set("ocean_treatment", ocean_treatment) { - return CommandResult::error(error.to_string()); - } - let normalized_theme = candidate.theme.clone(); - let normalized_treatment = candidate.ocean_treatment.clone(); - - // Resolve/apply the theme first: custom themes can fail resolution even - // after their selector syntax validates, and treatment must not change in - // that case. The treatment value has already passed Settings validation. - let theme_result = set_config_value(app, "theme", &normalized_theme, false); - if theme_result.is_error { - return theme_result; - } - let treatment_result = set_config_value(app, "ocean_treatment", &normalized_treatment, false); - if treatment_result.is_error { - return treatment_result; - } - - if persist - && let Err(error) = Settings::transact(|settings| { - settings.set("theme", &normalized_theme)?; - settings.set("ocean_treatment", &normalized_treatment) - }) - { - return CommandResult::error(format!("Failed to save: {error}")); - } - - CommandResult { - message: Some(format!( - "theme = {normalized_theme}, ocean_treatment = {normalized_treatment} ({})", - if persist { - "saved" - } else { - "session only, add --save to persist" - } - )), - action: theme_result.action.or(treatment_result.action), - is_error: false, - } -} - /// Modify a setting at runtime pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> CommandResult { let key = key.to_lowercase(); @@ -2553,11 +2487,6 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> app.fancy_animations = effective_settings.fancy_animations; app.needs_redraw = true; } - "ocean_treatment" | "treatment" | "background_treatment" => { - app.ocean_treatment = - crate::tui::ocean::OceanTreatment::parse(&settings.ocean_treatment); - app.needs_redraw = true; - } "focus_texture" | "texture" => { app.focus_texture = crate::tui::focus_texture::FocusTextureMode::parse(&settings.focus_texture) @@ -2966,6 +2895,9 @@ pub fn theme(app: &mut App, arg: Option<&str>) -> CommandResult { )), Err(error) => CommandResult::error(error), }, + // `underwater` is an ordinary theme (aliases `deepsea`/`deep-sea`/ + // `ombre` fold through the same normalizer); the painted ocean field + // is the theme itself, not a treatment beside it. Some(name) => set_config_value(app, "theme", name, true), } } @@ -4956,9 +4888,41 @@ context_window = 262144 } #[test] - fn compound_theme_selection_updates_live_state_and_persists_one_pair_transaction() { + fn theme_command_underwater_alias_selects_the_underwater_theme() { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); let temp_root = env::temp_dir().join(format!( - "codewhale-tui-deepsea-selection-test-{}-{}", + "codewhale-tui-theme-underwater-test-{}-{}", + std::process::id(), + nanos + )); + fs::create_dir_all(&temp_root).unwrap(); + let _guard = EnvGuard::new(&temp_root); + + let mut app = create_test_app(); + for alias in ["underwater", "Deepsea", "deep-sea", "ombre"] { + let result = theme(&mut app, Some(alias)); + assert!(!result.is_error, "{alias}: {:?}", result.message); + assert_eq!( + result.message.as_deref(), + Some("theme = underwater (saved)"), + "{alias}" + ); + assert_eq!(app.theme_id, crate::palette::ThemeId::Underwater, "{alias}"); + assert_eq!(app.ui_theme.name, "underwater", "{alias}"); + assert!( + crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_some(), + "{alias}: the underwater theme owns the painted field" + ); + } + } + + #[test] + fn underwater_theme_selection_updates_live_state_and_persists_one_field() { + let temp_root = env::temp_dir().join(format!( + "codewhale-tui-underwater-selection-test-{}-{}", std::process::id(), SystemTime::now() .duration_since(UNIX_EPOCH) @@ -4969,32 +4933,27 @@ context_window = 262144 let _guard = EnvGuard::new(&temp_root); fs::write( temp_root.join(".deepseek").join("settings.toml"), - "theme = \"light\"\nocean_treatment = \"flat\"\nmax_input_history = 77\n", + "theme = \"light\"\nmax_input_history = 77\n", ) .expect("seed settings"); let mut app = create_test_app(); - let result = set_theme_selection(&mut app, "dark", "deepsea", true); + let result = set_config_value(&mut app, "theme", "underwater", true); assert!(!result.is_error, "{:?}", result.message); - assert_eq!(app.theme_id, crate::palette::ThemeId::Whale); - assert_eq!( - app.ocean_treatment, - crate::tui::ocean::OceanTreatment::Deepsea - ); - let persisted = Settings::load_persisted().expect("persisted compound selection"); - assert_eq!(persisted.theme, "dark"); - assert_eq!(persisted.ocean_treatment, "deepsea"); + assert_eq!(app.theme_id, crate::palette::ThemeId::Underwater); + let persisted = Settings::load_persisted().expect("persisted selection"); + assert_eq!(persisted.theme, "underwater"); assert_eq!( persisted.max_input_history, 77, - "the compound transaction must not overwrite unrelated settings" + "the theme save must not overwrite unrelated settings" ); } #[test] - fn compound_theme_selection_preflights_both_fields_before_live_mutation() { + fn invalid_theme_name_changes_nothing() { let temp_root = env::temp_dir().join(format!( - "codewhale-tui-deepsea-preflight-test-{}-{}", + "codewhale-tui-theme-preflight-test-{}-{}", std::process::id(), SystemTime::now() .duration_since(UNIX_EPOCH) @@ -5005,21 +4964,18 @@ context_window = 262144 let _guard = EnvGuard::new(&temp_root); fs::write( temp_root.join(".deepseek").join("settings.toml"), - "theme = \"light\"\nocean_treatment = \"flat\"\n", + "theme = \"light\"\n", ) .expect("seed settings"); let mut app = create_test_app(); let original_theme = app.theme_id; - let original_treatment = app.ocean_treatment; - let result = set_theme_selection(&mut app, "dark", "kelp", true); + let result = set_config_value(&mut app, "theme", "kelp", true); assert!(result.is_error); assert_eq!(app.theme_id, original_theme); - assert_eq!(app.ocean_treatment, original_treatment); let persisted = Settings::load_persisted().expect("unchanged persisted settings"); assert_eq!(persisted.theme, "light"); - assert_eq!(persisted.ocean_treatment, "flat"); } #[test] @@ -5050,9 +5006,38 @@ context_window = 262144 assert_eq!(app.theme_id, crate::palette::ThemeId::Whale); assert_eq!(app.background_color_override, Some(explicit_base3)); assert_eq!(app.ui_theme.surface_bg, explicit_base3); + assert!( + crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_none(), + "only the underwater theme owns a painted field" + ); + } + + #[test] + fn underwater_theme_keeps_its_field_under_a_background_override() { + let temp_root = env::temp_dir().join(format!( + "codewhale-tui-underwater-override-test-{}-{}", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock") + .as_nanos() + )); + fs::create_dir_all(temp_root.join(".deepseek")).expect("settings dir"); + let _guard = EnvGuard::new(&temp_root); + + let mut app = create_test_app(); + let custom = ratatui::style::Color::Rgb(0x1a, 0x1b, 0x26); + let background = set_config_value(&mut app, "background_color", "#1a1b26", false); + assert!(!background.is_error, "{:?}", background.message); + + let preview = set_config_value(&mut app, "theme", "underwater", false); + assert!(!preview.is_error, "{:?}", preview.message); + assert_eq!(app.theme_id, crate::palette::ThemeId::Underwater); + assert_eq!(app.background_color_override, Some(custom)); + assert_eq!(app.ui_theme.surface_bg, custom); assert!( crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_some(), - "the explicit surface must retain Deepsea when previewing another theme" + "the underwater theme's field survives a background override" ); } @@ -5093,7 +5078,7 @@ context_window = 262144 ); assert_eq!(app.background_color_override, Some(custom)); assert_eq!(app.ui_theme.surface_bg, custom); - assert!(crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_some()); + assert!(crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_none()); let saved_theme = set_config_value(&mut app, "theme", "dark", true); assert!(!saved_theme.is_error, "{:?}", saved_theme.message); diff --git a/crates/tui/src/commands/groups/config/mod.rs b/crates/tui/src/commands/groups/config/mod.rs index d626f58b3a..fa243419be 100644 --- a/crates/tui/src/commands/groups/config/mod.rs +++ b/crates/tui/src/commands/groups/config/mod.rs @@ -117,7 +117,7 @@ static INLINE_INFO: CommandInfo = CommandInfo { static THEME_INFO: CommandInfo = CommandInfo { name: "theme", aliases: &[], - usage: "/theme [name|custom:|schema|path]", + usage: "/theme [name|underwater|custom:|schema|path]", description_id: MessageId::CmdThemeDescription, }; static VERBOSE_INFO: CommandInfo = CommandInfo { diff --git a/crates/tui/src/commands/groups/core/core.rs b/crates/tui/src/commands/groups/core/core.rs index e4a0a3bff6..e1f0e9d437 100644 --- a/crates/tui/src/commands/groups/core/core.rs +++ b/crates/tui/src/commands/groups/core/core.rs @@ -1700,7 +1700,7 @@ mod tests { let result = home_dashboard(&mut app); assert!(result.message.is_some()); let msg = result.message.unwrap(); - assert!(msg.contains("Codewhale")); + assert!(msg.contains("codewhale")); assert!(!msg.contains("codewhale Home Dashboard")); assert!(msg.contains("Model:")); assert!(msg.contains("Mode:")); @@ -1772,7 +1772,7 @@ mod tests { .message .expect("home dashboard should return message"); assert!( - msg.contains("Codewhale"), + msg.contains("codewhale"), "missing canonical product title:\n{msg}" ); assert!(msg.contains("模型"), "missing zh-Hans model label:\n{msg}"); diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index 8d8083553b..93b652925f 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -328,16 +328,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> groups::config::config::set_config_value(app, key, value, persist) } -/// Update the canonical theme + ocean-treatment selection as one operation. -pub fn set_theme_selection( - app: &mut App, - theme: &str, - ocean_treatment: &str, - persist: bool, -) -> CommandResult { - groups::config::config::set_theme_selection(app, theme, ocean_treatment, persist) -} - +/// Switch the interaction mode (plan / work / operate). pub fn switch_mode(app: &mut App, mode: crate::tui::app::AppMode) -> String { groups::config::config::switch_mode(app, mode) } diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index b95c045ed0..d7f97adeea 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -62,7 +62,6 @@ pub struct SettingsSection { pub calm_mode: bool, pub low_motion: bool, pub fancy_animations: bool, - pub ocean_treatment: OceanTreatmentValue, pub focus_texture: FocusTextureValue, pub work_surface_placement: WorkSurfacePlacementValue, #[schemars(range(min = 2, max = 16))] @@ -269,14 +268,6 @@ pub enum UiThemeValue { Custom, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum OceanTreatmentValue { - #[serde(alias = "ombre", alias = "underwater")] - Deepsea, - Flat, -} - #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum FocusTextureValue { @@ -437,7 +428,6 @@ pub fn build_document(app: &App, config: &Config) -> Result { calm_mode: settings.calm_mode, low_motion: settings.low_motion, fancy_animations: settings.fancy_animations, - ocean_treatment: settings.ocean_treatment.as_str().into(), focus_texture: settings.focus_texture.as_str().into(), work_surface_placement: settings.work_surface_placement.as_str().into(), work_surface_top_height: settings.work_surface_top_height, @@ -630,7 +620,6 @@ pub fn apply_document( ("calm_mode", bool_str(doc.settings.calm_mode)), ("low_motion", bool_str(doc.settings.low_motion)), ("fancy_animations", bool_str(doc.settings.fancy_animations)), - ("ocean_treatment", doc.settings.ocean_treatment.as_setting()), ("focus_texture", doc.settings.focus_texture.as_setting()), ( "work_surface_placement", @@ -1134,24 +1123,6 @@ impl UiThemeValue { } } -impl OceanTreatmentValue { - fn as_setting(self) -> &'static str { - match self { - Self::Deepsea => "deepsea", - Self::Flat => "flat", - } - } -} - -impl From<&str> for OceanTreatmentValue { - fn from(value: &str) -> Self { - match value.trim().to_ascii_lowercase().as_str() { - "deepsea" | "ombre" | "gradient" | "classic" => Self::Deepsea, - _ => Self::Flat, - } - } -} - impl FocusTextureValue { fn as_setting(self) -> &'static str { match self { diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index e0b91796ae..956f9aaa8d 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -635,7 +635,7 @@ impl Engine { // app-server, and stream-json stdout must remain byte-clean. if self.config.terminal_chrome_enabled { crate::tui::notifications::set_taskbar_progress_busy(); - crate::tui::notifications::start_title_animation("Codewhale"); + crate::tui::notifications::start_title_animation("codewhale"); } let client = self diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 1a8e71609c..ee7d391255 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -281,7 +281,6 @@ pub enum MessageId { ConfigLabelTheme, ConfigLabelLocale, ConfigLabelBackground, - ConfigLabelOceanTreatment, ConfigLabelWorkSurfacePlacement, ConfigLabelTopHeight, ConfigLabelSideWidth, @@ -1429,6 +1428,7 @@ pub enum MessageId { LaunchSavedSessionsPlural, LaunchCreatingWorktree, LaunchWorktreeFailed, + LaunchWorktreeCreated, LaunchNoSavedSessions, LaunchComposerHint, LaunchNoModelConnected, @@ -1727,9 +1727,6 @@ pub enum MessageId { ProviderExternalRevokeFailedToast, // Theme picker surface. ThemeSurfaceTitle, - ThemeTreatmentDeepseaUnavailable, - ThemeTreatmentFlatActive, - ThemeTreatmentDeepseaActive, // Fleet roster room. FleetRosterHeaderLabel, FleetRosterTabRoster, @@ -2175,8 +2172,6 @@ pub enum MessageId { ConfigChoiceDetailShowThinkingOff, ConfigChoiceDetailThinkingHighlightOn, ConfigChoiceDetailThinkingHighlightOff, - ConfigChoiceDetailOceanDeepsea, - ConfigChoiceDetailOceanFlat, ConfigHintModel, ConfigHintFastModel, ConfigHintProvider, @@ -2203,7 +2198,6 @@ pub enum MessageId { ConfigHintCalmMode, ConfigHintLowMotion, ConfigHintFancyAnimations, - ConfigHintOceanTreatment, ConfigHintShowThinking, ConfigHintThinkingDefaultExpanded, ConfigHintThinkingPreviewLines, @@ -2396,7 +2390,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::ConfigLabelTheme, MessageId::ConfigLabelLocale, MessageId::ConfigLabelBackground, - MessageId::ConfigLabelOceanTreatment, MessageId::ConfigLabelWorkSurfacePlacement, MessageId::ConfigLabelTopHeight, MessageId::ConfigLabelSideWidth, @@ -3501,6 +3494,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::LaunchSavedSessionsPlural, MessageId::LaunchCreatingWorktree, MessageId::LaunchWorktreeFailed, + MessageId::LaunchWorktreeCreated, MessageId::LaunchNoSavedSessions, MessageId::LaunchComposerHint, MessageId::LaunchNoModelConnected, @@ -3784,9 +3778,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::ProviderExternalRevokedToast, MessageId::ProviderExternalRevokeFailedToast, MessageId::ThemeSurfaceTitle, - MessageId::ThemeTreatmentDeepseaUnavailable, - MessageId::ThemeTreatmentFlatActive, - MessageId::ThemeTreatmentDeepseaActive, MessageId::FleetRosterHeaderLabel, MessageId::FleetRosterTabRoster, MessageId::FleetRosterTabSetup, @@ -4205,8 +4196,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::ConfigChoiceDetailShowThinkingOff, MessageId::ConfigChoiceDetailThinkingHighlightOn, MessageId::ConfigChoiceDetailThinkingHighlightOff, - MessageId::ConfigChoiceDetailOceanDeepsea, - MessageId::ConfigChoiceDetailOceanFlat, MessageId::ConfigHintModel, MessageId::ConfigHintFastModel, MessageId::ConfigHintProvider, @@ -4233,7 +4222,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::ConfigHintCalmMode, MessageId::ConfigHintLowMotion, MessageId::ConfigHintFancyAnimations, - MessageId::ConfigHintOceanTreatment, MessageId::ConfigHintShowThinking, MessageId::ConfigHintThinkingDefaultExpanded, MessageId::ConfigHintThinkingPreviewLines, diff --git a/crates/tui/src/palette/themes.rs b/crates/tui/src/palette/themes.rs index f61bcf48dc..f385285cbf 100644 --- a/crates/tui/src/palette/themes.rs +++ b/crates/tui/src/palette/themes.rs @@ -139,6 +139,81 @@ pub const UI_THEME: UiTheme = UiTheme { } .with_terminal_native_shell(); +/// The underwater theme: the one theme that owns a painted ground. The ocean +/// ramp (`crate::tui::ocean::OceanRamp`) paints the water column over these +/// surfaces; the values below are what shows through before the first paint +/// and on rows the ramp does not reach. Ink/accent roles match the dark whale +/// pair so text contrast is identical. +pub const UNDERWATER_UI_THEME: UiTheme = UiTheme { + name: "underwater", + mode: PaletteMode::Dark, + surface_bg: Color::Rgb(0x0a, 0x1e, 0x33), + panel_bg: Color::Rgb(0x0d, 0x22, 0x3a), + elevated_bg: Color::Rgb(0x10, 0x2a, 0x45), + composer_bg: Color::Rgb(0x0d, 0x22, 0x3a), + selection_bg: SELECTION_BG, + header_bg: Color::Rgb(0x06, 0x13, 0x20), + footer_bg: Color::Rgb(0x06, 0x13, 0x20), + text_dim: TEXT_DIM, + text_hint: TEXT_HINT, + text_muted: TEXT_MUTED, + text_body: TEXT_BODY, + text_soft: TEXT_SOFT, + border: BORDER_COLOR, + accent_primary: WHALE_ACTION, + accent_secondary: WHALE_LIVE, + accent_action: WHALE_HUMAN, + error_fg: WHALE_ERROR, + error_hover: Color::Rgb( + WHALE_ERROR_HOVER_RGB.0, + WHALE_ERROR_HOVER_RGB.1, + WHALE_ERROR_HOVER_RGB.2, + ), + error_surface: Color::Rgb( + WHALE_ERROR_SURFACE_RGB.0, + WHALE_ERROR_SURFACE_RGB.1, + WHALE_ERROR_SURFACE_RGB.2, + ), + error_border: Color::Rgb( + WHALE_ERROR_BORDER_RGB.0, + WHALE_ERROR_BORDER_RGB.1, + WHALE_ERROR_BORDER_RGB.2, + ), + error_text: Color::Rgb( + WHALE_ERROR_TEXT_RGB.0, + WHALE_ERROR_TEXT_RGB.1, + WHALE_ERROR_TEXT_RGB.2, + ), + warning: STATUS_WARNING, + success: Color::Rgb( + WHALE_SUCCESS_RGB.0, + WHALE_SUCCESS_RGB.1, + WHALE_SUCCESS_RGB.2, + ), + info: WHALE_ACTION, + mode_agent: MODE_AGENT, + mode_yolo: MODE_YOLO, + mode_plan: MODE_PLAN, + mode_operate: MODE_OPERATE, + permission_ask: TEXT_REASONING, + permission_auto_review: WHALE_HUMAN, + permission_full_access: STATUS_WARNING, + status_ready: TEXT_MUTED, + status_working: WHALE_LIVE, + status_warning: STATUS_WARNING, + diff_added_fg: DIFF_ADDED, + diff_deleted_fg: WHALE_ERROR, + diff_added_bg: DIFF_ADDED_BG, + diff_deleted_bg: DIFF_DELETED_BG, + tool_running: WHALE_LIVE, + tool_success: Color::Rgb( + WHALE_WORKING_GREEN_RGB.0, + WHALE_WORKING_GREEN_RGB.1, + WHALE_WORKING_GREEN_RGB.2, + ), + tool_failed: WHALE_ERROR, +}; + pub const LIGHT_UI_THEME: UiTheme = UiTheme { name: "whale-light", mode: PaletteMode::Light, @@ -738,6 +813,7 @@ pub const UWU_UI_THEME: UiTheme = UiTheme { pub enum ThemeId { System, Terminal, + Underwater, Whale, WhaleLight, Grayscale, @@ -760,6 +836,7 @@ impl ThemeId { match normalize_theme_name(value)? { "system" => Some(Self::System), "terminal" => Some(Self::Terminal), + "underwater" | "deepsea" => Some(Self::Underwater), "dark" => Some(Self::Whale), "light" => Some(Self::WhaleLight), "grayscale" => Some(Self::Grayscale), @@ -782,6 +859,7 @@ impl ThemeId { match self { Self::System => "system", Self::Terminal => "terminal", + Self::Underwater => "underwater", Self::Whale => "dark", Self::WhaleLight => "light", Self::Grayscale => "grayscale", @@ -802,6 +880,7 @@ impl ThemeId { match self { Self::System => "System", Self::Terminal => "Terminal", + Self::Underwater => "Underwater", Self::Whale => "Blue Stage", Self::WhaleLight => "Blue Stage Light", Self::Grayscale => "Grayscale", @@ -822,6 +901,7 @@ impl ThemeId { match self { Self::System => "Follow terminal background (COLORFGBG / macOS appearance)", Self::Terminal => "Inherit terminal colors fully (transparent surfaces, ANSI accents)", + Self::Underwater => "The painted ocean field: ombre water, ambient life, the whale", Self::Whale => "Stage black, action blue, and one Signal Gold human beacon", Self::WhaleLight => "Paper, cobalt action, and one Signal Gold human beacon", Self::Grayscale => "Color-minimal high contrast", @@ -847,6 +927,7 @@ impl ThemeId { match self { Self::System => UiTheme::detect(), Self::Terminal => TERMINAL_UI_THEME, + Self::Underwater => UNDERWATER_UI_THEME, Self::Whale => UI_THEME, Self::WhaleLight => LIGHT_UI_THEME, Self::Grayscale => GRAYSCALE_UI_THEME, @@ -866,6 +947,7 @@ impl ThemeId { pub const SELECTABLE_THEMES: &[ThemeId] = &[ ThemeId::System, ThemeId::Terminal, + ThemeId::Underwater, ThemeId::Whale, ThemeId::WhaleLight, ThemeId::Grayscale, @@ -924,6 +1006,7 @@ pub fn normalize_theme_name(value: &str) -> Option<&'static str> { match value.trim().to_ascii_lowercase().as_str() { "" | "auto" | "system" | "default" => Some("system"), "terminal" | "term" | "transparent" | "follow-terminal" | "inherit" => Some("terminal"), + "underwater" | "deepsea" | "deep-sea" | "ocean" | "ombre" => Some("underwater"), "dark" | "whale" | "whale-dark" => Some("dark"), "light" | "whale-light" => Some("light"), "grayscale" | "greyscale" | "gray" | "grey" | "mono" | "monochrome" | "black-white" @@ -999,6 +1082,7 @@ mod tests { [ "system", "terminal", + "underwater", "dark", "light", "grayscale", @@ -1014,6 +1098,10 @@ mod tests { ); assert_eq!(normalize_theme_name("default"), Some("system")); assert_eq!(normalize_theme_name("whale"), Some("dark")); + // The retired treatment spellings are theme names now. + assert_eq!(normalize_theme_name("underwater"), Some("underwater")); + assert_eq!(normalize_theme_name("deepsea"), Some("underwater")); + assert_eq!(normalize_theme_name("ombre"), Some("underwater")); assert_eq!(normalize_theme_name("owo"), Some("uwu")); assert_eq!(normalize_theme_name("kawaii"), Some("uwu")); } diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index c02fcb1795..7033271bdd 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -314,9 +314,6 @@ pub struct Settings { /// Enable expressive live-state motion. This affects chrome and state /// affordances only; model text always follows upstream stream deltas. pub fancy_animations: bool, - /// Background treatment: `deepsea` paints the terminal-native water column; - /// `flat` preserves all state marks on the theme's plain surface. - pub ocean_treatment: String, /// Focus-context texture prototype for modal views (#4823): `off` /// (default), `scrim` dims the area outside the focused modal, `grain` /// sprinkles deterministic dots over blank cells there. Static texture, @@ -562,11 +559,8 @@ impl Default for Settings { low_motion: false, load_error: None, fancy_animations: true, - // A fresh terminal follows the host surface. Deep/ocean treatment - // remains an explicit appearance choice rather than a backdrop - // painted over every terminal the user brings. - ocean_treatment: "flat".to_string(), focus_texture: "off".to_string(), + // Round 3 (2026-09-01): the bar's information lives under the // composer. Side rails are opt-in and fall back to the top strip // on narrow terminals. @@ -647,13 +641,6 @@ pub const CALM_PRESET_FIELDS: &[(&str, &str)] = &[ ("show_tool_details", "false"), ]; -fn normalize_ocean_treatment(value: &str) -> &'static str { - match value.trim().to_ascii_lowercase().as_str() { - "deepsea" | "underwater" | "ombre" | "gradient" | "classic" => "deepsea", - _ => "flat", - } -} - fn normalize_work_surface_placement(value: &str) -> &'static str { match value.trim().to_ascii_lowercase().as_str() { "top" => "top", @@ -916,6 +903,27 @@ impl Settings { }) { s.auto_compact = true; } + + // Compat boundary (2026-09-02): `ocean_treatment` was a modifier + // on `theme`; the painted field is now the `underwater` theme + // itself. Fold any persisted deepsea treatment into + // `theme = "underwater"`, then drop the retired key on the next + // ordinary save (the struct simply has no such field). + if let Some(_treatment) = parsed_document + .as_ref() + .and_then(toml::Value::as_table) + .and_then(|table| table.get("ocean_treatment")) + .and_then(toml::Value::as_str) + .filter(|treatment| { + matches!( + treatment.trim().to_ascii_lowercase().as_str(), + "deepsea" | "underwater" | "ombre" | "gradient" | "classic" + ) + }) + { + s.theme = "underwater".to_string(); + } + // "yolo" used to bundle two independent choices: Agent mode and // unrestricted approvals. Keep that behavior on upgrade, but // store/show the two choices explicitly so Settings does not claim @@ -935,7 +943,6 @@ impl Settings { // gone, so its settings carry forward instead of stranding. migrate_sidebar_settings_to_rail(&mut s); s.status_indicator = normalize_status_indicator(&s.status_indicator).to_string(); - s.ocean_treatment = normalize_ocean_treatment(&s.ocean_treatment).to_string(); s.work_surface_placement = normalize_work_surface_placement(&s.work_surface_placement).to_string(); s.rail_panel = normalize_rail_panel(&s.rail_panel).to_string(); @@ -1334,18 +1341,6 @@ impl Settings { "fancy_animations" | "fancy" | "animations" => { self.fancy_animations = parse_bool(value)?; } - "ocean_treatment" | "treatment" | "background_treatment" => { - let normalized = value.trim().to_ascii_lowercase(); - self.ocean_treatment = match normalized.as_str() { - "deepsea" | "underwater" | "ombre" | "gradient" | "classic" => { - "deepsea".to_string() - } - "flat" | "terminal" | "none" => "flat".to_string(), - _ => anyhow::bail!( - "Failed to update setting: invalid ocean treatment '{value}'. Expected: deepsea or flat." - ), - }; - } "focus_texture" | "texture" => { let normalized = value.trim().to_ascii_lowercase(); if !matches!(normalized.as_str(), "off" | "scrim" | "grain") { @@ -1635,7 +1630,6 @@ impl Settings { lines.push(format!(" tool_collapse: {}", self.tool_collapse_mode)); lines.push(format!(" low_motion: {}", self.low_motion)); lines.push(format!(" fancy_animations: {}", self.fancy_animations)); - lines.push(format!(" ocean_treatment: {}", self.ocean_treatment)); lines.push(format!(" focus_texture: {}", self.focus_texture)); lines.push(format!( " work_surface: {}", @@ -1775,10 +1769,6 @@ impl Settings { "Reduce decorative motion without changing model text delivery: on/off", ), ("fancy_animations", "Expressive live-state motion: on/off"), - ( - "ocean_treatment", - "Transcript background treatment: deepsea/flat (independent of motion)", - ), ( "focus_texture", "Modal focus-context texture prototype: off/scrim/grain (default off)", @@ -3155,29 +3145,50 @@ mod tests { } #[test] - fn ocean_treatment_is_appearance_not_motion() { - let mut settings = Settings::default(); - assert_eq!(settings.ocean_treatment, "flat"); - assert!(!settings.low_motion); + fn retired_ocean_treatment_folds_into_the_underwater_theme() { + let tmp = tempfile::tempdir().expect("tempdir"); + let path = tmp.path().join("settings.toml"); + std::fs::write(&path, "theme = \"light\"\nocean_treatment = \"deepsea\"\n") + .expect("legacy settings"); + + let settings = Settings::load_persisted_from_candidates(Some(path.clone()), None, None) + .expect("legacy setting must remain readable"); + assert_eq!( + settings.theme, "underwater", + "the persisted painted field is the user-visible fact; it becomes the theme" + ); + + settings + .save_to_path(&path) + .expect("save normalized settings"); + let saved = std::fs::read_to_string(&path).expect("read normalized settings"); + assert!( + !saved.contains("ocean_treatment"), + "the retired key must not be written back: {saved}" + ); + assert!(saved.contains("theme = \"underwater\""), "{saved}"); + } - settings.set("ocean_treatment", "flat").unwrap(); - assert_eq!(settings.ocean_treatment, "flat"); - assert!(!settings.low_motion, "appearance must not change motion"); + #[test] + fn flat_ocean_treatment_leaves_the_theme_alone_and_is_dropped() { + let tmp = tempfile::tempdir().expect("tempdir"); + let path = tmp.path().join("settings.toml"); + std::fs::write(&path, "theme = \"light\"\nocean_treatment = \"flat\"\n") + .expect("legacy settings"); - settings.set("ocean_treatment", "deepsea").unwrap(); - assert_eq!(settings.ocean_treatment, "deepsea"); - settings.set("ocean_treatment", "ombre").unwrap(); + let settings = Settings::load_persisted_from_candidates(Some(path.clone()), None, None) + .expect("legacy setting must remain readable"); assert_eq!( - settings.ocean_treatment, "deepsea", - "legacy values migrate one way to the public Deepsea contract" + settings.theme, "light", + "flat never opted into a painted field" ); - settings.set("ocean_treatment", "underwater").unwrap(); - assert_eq!(settings.ocean_treatment, "deepsea"); - assert_eq!(normalize_ocean_treatment("Underwater"), "deepsea"); - assert_eq!(normalize_ocean_treatment("kelp"), "flat"); - let err = settings.set("ocean_treatment", "kelp").unwrap_err(); - assert!(err.to_string().contains("deepsea or flat")); + settings + .save_to_path(&path) + .expect("save normalized settings"); + let saved = std::fs::read_to_string(&path).expect("read normalized settings"); + assert!(!saved.contains("ocean_treatment"), "{saved}"); + assert!(saved.contains("theme = \"light\""), "{saved}"); } #[test] diff --git a/crates/tui/src/tui/ambient_life.rs b/crates/tui/src/tui/ambient_life.rs index a22e754838..af2aede4e9 100644 --- a/crates/tui/src/tui/ambient_life.rs +++ b/crates/tui/src/tui/ambient_life.rs @@ -353,10 +353,9 @@ fn build_frame_marks( // Half-cycle head start: freshly opened water shows the school // mid-crossing instead of an empty entry beat. let school_clock = t.saturating_add(cycle_ms / 2); - let (cycle_index, cycle_step) = ( - school_clock / cycle_ms, - ((school_clock % cycle_ms) / SCHOOL_CELL_MS) as i32, - ); + let cycle_index = school_clock / cycle_ms; + let cycle_frac = (school_clock % cycle_ms) as f64 / cycle_ms as f64; + let cycle_step = (cycle_frac * travel as f64).round() as i32; let swims_right = school_swims_right(cycle_index); // The school has one home: the deep water just off the floor. It used to // alternate between an upper and a lower band, which is most of why the @@ -473,11 +472,14 @@ fn build_frame_marks( // per-row dwell stays long — a jellyfish should read as drifting, not // as stepping. let rise_period = JELLY_RISE_ROW_MS.saturating_add((j as u128) * JELLY_RISE_ROW_STAGGER_MS); - let slot = (t.saturating_add(phase) / rise_period) % JELLY_VISIT_CYCLE_SLOTS; - if slot >= u128::from(JELLY_VISIT_ROWS) { + let cycle_duration = rise_period.saturating_mul(JELLY_VISIT_CYCLE_SLOTS); + let cycle_pos = t.saturating_add(phase) % cycle_duration; + let visit_duration = rise_period.saturating_mul(u128::from(JELLY_VISIT_ROWS)); + if cycle_pos >= visit_duration { continue; // still down in the dark between visits } - let risen = slot as u16; + let visit_progress = cycle_pos as f64 / visit_duration as f64; + let risen = (visit_progress * f64::from(JELLY_VISIT_ROWS)).round() as u16; let y = area .height .saturating_sub(JELLY_FLOOR_GAP) @@ -485,15 +487,17 @@ fn build_frame_marks( if y == 0 || !water(x, y, dome_w) { continue; } - let dome_brightness = jelly_glow(wave01(t, JELLY_PULSE_MS, phase)); - let tentacle_brightness = jelly_glow(wave01( + let dome_pulse = wave01(t, JELLY_PULSE_MS, phase); + let dome_brightness = jelly_glow(dome_pulse); + let tentacle_pulse = wave01( t.saturating_sub(JELLY_TENTACLE_LAG_MS), JELLY_PULSE_MS, phase, - )); - // The dome opens/closes on the same clock as its glow; the parked + ); + let tentacle_brightness = jelly_glow(tentacle_pulse); + // The dome opens/closes on the smooth continuous phase curve; the parked // pose holds the half-pulsed (contracted) frame. - let pulse_frame = usize::from(wave01(t, JELLY_PULSE_MS, phase) > 0.5); + let pulse_frame = usize::from(dome_pulse > 0.5); let skirt_row = y.saturating_add(1); let tentacle_row = y.saturating_add(2); // Treat the silhouette as one visual unit. The former per-row quiet @@ -545,13 +549,9 @@ fn build_frame_marks( } } - // --- Rising bubble streams: a short run off the floor, then dissolve --- - // A bubble used to travel the whole column and then clamp at the top of - // the field, where it parked as a single unattached speck for the rest of - // its cycle — the `·` sitting at column 11 doing nothing in the 80×24 - // frame. It now rises [`BUBBLE_MAX_RISE_ROWS`] rows from the floor, - // grows, and fades out, which is both what a bubble does and a reason for - // it to be exactly where it is. + // --- Marine snow & rising bubble streams floating upward --- + // Floating particles rise smoothly through the water column, dissolving + // gently with continuous time-based floating physics. for b in 0..density.bubble_streams() { let phase = (b as u128).saturating_mul(1_900); // Edge columns — avoid center brand. @@ -567,16 +567,19 @@ fn build_frame_marks( } else { 0 }; - let rise = ((cycle * f64::from(BUBBLE_MAX_RISE_ROWS)) as u16) + // Continuous horizontal floating drift + let drift_phase = (t.saturating_add(phase) as f64 / 2_100.0) * std::f64::consts::TAU; + let drift = (drift_phase.sin() * 0.6).round() as i16; + let col = (column as i16 + drift).clamp(0, (area.width.saturating_sub(1)) as i16) as u16; + + let rise = ((cycle * f64::from(BUBBLE_MAX_RISE_ROWS)).round() as u16) .saturating_add(boost) .min(BUBBLE_MAX_RISE_ROWS); let y = area.height.saturating_sub(2).saturating_sub(rise); - if !water(column, y, 1) { + if !water(col, y, 1) { continue; } - // Size is a function of height risen, not of the clock: the old - // `["·", "˚", "·", "°"]` table swapped glyph every 320 ms in place, - // which is a flicker in peripheral vision rather than a rise. + // Size is a function of height risen, not of discrete clock jumps. let glyph = bubble_glyph(rise); let brightness = glint01( t, @@ -586,7 +589,7 @@ fn build_frame_marks( phase, ) * bubble_dissolve(rise); marks.push(AmbientMark { - x: column.min(area.width.saturating_sub(1)), + x: col, y, glyph, jellyfish: None, @@ -609,9 +612,13 @@ fn build_frame_marks( if phase == WhaleCameoPhase::Hidden { continue; } + // Smooth continuous forward swimming drift across cameo + let cameo_frac = pod_cameo_ms as f64 / WHALE_CAMEO_MS as f64; + let drift = (cameo_frac * 3.0).round() as u16; let ax = whale .anchor_x .saturating_add_signed(*offset) + .saturating_add(drift) .saturating_sub(area.x) .min(area.width.saturating_sub(4)); let ay = whale @@ -625,6 +632,10 @@ fn build_frame_marks( WhaleCameoPhase::Submerge => ("·", 1), WhaleCameoPhase::Hidden => ("", 0), }; + let whale_glow = { + let s = (cameo_frac * std::f64::consts::PI).sin(); + (0.65 + 0.35 * s) as f32 + }; if !glyph.is_empty() { marks.push(AmbientMark { x: ax, @@ -633,7 +644,7 @@ fn build_frame_marks( jellyfish: None, depth: Depth::Foreground, style_mod: None, - brightness: None, + brightness: Some(whale_glow), }); if phase == WhaleCameoPhase::Spout && ay > 0 { marks.push(AmbientMark { @@ -643,7 +654,7 @@ fn build_frame_marks( jellyfish: None, depth: Depth::Foreground, style_mod: Some(Modifier::DIM), - brightness: None, + brightness: Some(whale_glow), }); } } diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index d02a6fcd09..1a3eb33a21 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -556,8 +556,11 @@ pub struct LaunchState { /// composed message through the normal dispatch path. pub send_area: Option, /// The launch card's highlighted menu entry (index into the four entries - /// the card paints, all of whose chords exist). - pub menu_selected: usize, + /// the card paints, all of whose chords exist). `None` until the user + /// arrows onto the menu: nothing is pre-selected, so a reflexive Enter at + /// launch does nothing rather than running "New worktree" (founder + /// live-test, 2026-09-02). Esc clears it again. + pub menu_selected: Option, /// Ambient-clock millisecond reading when the card began dissolving, if /// it has. The first keystroke or a launched command dissolves the card /// (founder decision, 2026-09-02). @@ -616,7 +619,7 @@ impl LaunchState { composer_focus: true, composer_area: None, send_area: None, - menu_selected: 0, + menu_selected: None, dissolve_started_ms: None, claude_code_detected, } @@ -630,6 +633,16 @@ impl LaunchState { } } + /// Bring the card back after a launch flow (resume picker, changelog, + /// worktree prompt) is left with Esc: every launch path has a way back + /// to the card, so a dismissed picker never strands the user on an empty + /// stage. The menu comes back with nothing highlighted. + pub fn restore_card(&mut self) { + self.dissolve_started_ms = None; + self.menu_selected = None; + self.status = None; + } + /// How far the card has dissolved, `[0.0 intact ..= 1.0 gone]`. Reduced /// motion dissolves instantly: the same drawing at its endpoint. #[must_use] @@ -1610,7 +1623,6 @@ pub struct App { pub fancy_animations: bool, /// Typed appearance treatment; appearance is independent from motion /// settings, and every underwater treatment keeps ambient life. - pub ocean_treatment: crate::tui::ocean::OceanTreatment, /// Focus-context texture prototype mode (#4823), parsed once from the /// `focus_texture` setting. `Off` by default; while off the modal render /// path is byte-identical to the pre-prototype path. diff --git a/crates/tui/src/tui/app/init.rs b/crates/tui/src/tui/app/init.rs index 4a79246d16..99fb263782 100644 --- a/crates/tui/src/tui/app/init.rs +++ b/crates/tui/src/tui/app/init.rs @@ -270,7 +270,6 @@ impl App { let low_motion = settings.low_motion; let constrained_frame_rate = settings.constrained_frame_rate; let fancy_animations = settings.fancy_animations; - let ocean_treatment = crate::tui::ocean::OceanTreatment::parse(&settings.ocean_treatment); let focus_texture = crate::tui::focus_texture::FocusTextureMode::parse(&settings.focus_texture) .unwrap_or_default(); @@ -862,7 +861,6 @@ impl App { ocean_turn_history_start: 0, ocean_receipt_settle_start: None, fancy_animations, - ocean_treatment, focus_texture, launch, pending_launch_action: None, diff --git a/crates/tui/src/tui/goldens/config_panel_120x32.txt b/crates/tui/src/tui/goldens/config_panel_120x32.txt index 5a2d988f6e..0cf8cb9b45 100644 --- a/crates/tui/src/tui/goldens/config_panel_120x32.txt +++ b/crates/tui/src/tui/goldens/config_panel_120x32.txt @@ -2,29 +2,29 @@ Config ──────────────────────────────────────────────────────────────────────────────────────────────────────────── Appearance Models & providers Fleet Work Tools & MCP Trust Motion Advanced - Search: type to filter (17/55) + Search: type to filter (16/54) ❯ Display │ Display │Theme │❯Theme terminal ‹ › │theme │ Language auto ‹ › │ │ Background (default) ✎ │current terminal - │ Ocean treatment flat ‹ › │saved terminal - │ Quiet transcript On [x] │startup terminal - │ Model reasoning in chat Off [ ] │source settings.toml - │ Thinking Default Expanded Off [ ] │scope SAVED - │ Thinking Preview Lines 2 ✎ │apply applies on save - │ Reasoning background highlight On [x] │kind choice - │ Help Expand Groups Off [ ] │available not observed this session - │ Pin Last Prompt On [x] │ - │ Tool detail level Off [ ] │system | terminal | dark | light | - │ Inline file changes Full diff ‹ › │grayscale | catppuccin-mocha | - │ Output pacing auto ‹ › │tokyo-night | dracula | gruvbox-dark | - │ Cost currency usd ‹ › │claude | matrix | solarized-light | uwu - │ Transcript spacing comfort... ‹ › │Enter or click again: Enter opens - │ Tool cards compact ‹ › │choices + │ Quiet transcript On [x] │saved terminal + │ Model reasoning in chat Off [ ] │startup terminal + │ Thinking Default Expanded Off [ ] │source settings.toml + │ Thinking Preview Lines 2 ✎ │scope SAVED + │ Reasoning background highlight On [x] │apply applies on save + │ Help Expand Groups Off [ ] │kind choice + │ Pin Last Prompt On [x] │available not observed this session + │ Tool detail level Off [ ] │ + │ Inline file changes Full diff ‹ › │system | terminal | underwater | dark | + │ Output pacing auto ‹ › │light | grayscale | catppuccin-mocha | + │ Cost currency usd ‹ › │tokyo-night | dracula | gruvbox-dark | + │ Transcript spacing comfort... ‹ › │claude | matrix | solarized-light | uwu + │ Tool cards compact ‹ › │Enter or click again: Enter opens + │ │choices │ │ │ │ │ │ - system | terminal | dark | light | grayscale | catppuccin-mocha | tokyo-night | dracula | gruvbox-dark | claude |… + system | terminal | underwater | dark | light | grayscale | catppuccin-mocha | tokyo-night | dracula |… Preview: ▶▶ ask · agent type=filter, Up/Down=select, Enter/e=edit, Esc/q=close diff --git a/crates/tui/src/tui/goldens/config_panel_80x24.txt b/crates/tui/src/tui/goldens/config_panel_80x24.txt index 039df2d50c..3071fd345b 100644 --- a/crates/tui/src/tui/goldens/config_panel_80x24.txt +++ b/crates/tui/src/tui/goldens/config_panel_80x24.txt @@ -2,19 +2,19 @@ Config ──────────────────────────────────────────────────────────────────── Appearance Models & providers Fleet Work Tools & MCP Trust › - Search: type to filter (17/55) + Search: type to filter (16/54) Display █ ❯Theme terminal ‹ › SAVED █ Language auto ‹ › SAVED █ Background (default) ✎ SAVED █ - Ocean treatment flat ‹ › SAVED █ Quiet transcript On [x] SAVED █ Model reasoning in chat Off [ ] SAVED █ - Thinking Default Expanded Off [ ] SAVED │ - Thinking Preview Lines 2 ✎ SAVED │ + Thinking Default Expanded Off [ ] SAVED █ + Thinking Preview Lines 2 ✎ SAVED █ Reasoning background highlight On [x] SAVED │ Help Expand Groups Off [ ] SAVED │ - system | terminal | dark | light | grayscale | catppuccin-mocha |… + Pin Last Prompt On [x] SAVED │ + system | terminal | underwater | dark | light | grayscale |… Enter or click again: Enter opens choices · Theme: current terminal · saved terminal · applies on save Preview: ▶▶ ask · agent diff --git a/crates/tui/src/tui/goldens/settings_100x30.txt b/crates/tui/src/tui/goldens/settings_100x30.txt index e944c5fec3..c52f2a5a1b 100644 --- a/crates/tui/src/tui/goldens/settings_100x30.txt +++ b/crates/tui/src/tui/goldens/settings_100x30.txt @@ -1,11 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Fleet Blue Stage ├── whale-1 · footer band - Work [ ✓ Blue Stage Light ] └── whale-2 · goldens - Tools & MCP Grayscale ● working whale-1 editing · 14:41:02 × - Trust Catppuccin Mocha ✓ done whale-2 surfaced ✓ · 14:39:02 × - Motion Tokyo Night done: stage restyled - Advanced Dracula + Fleet Underwater ├── whale-1 · footer band + Work [ ✓ Blue Stage ] └── whale-2 · goldens + Tools & MCP Blue Stage Light ● working whale-1 editing · 14:41:02 × + Trust Grayscale ✓ done whale-2 surfaced ✓ · 14:39:02 × + Motion Catppuccin Mocha done: stage restyled + Advanced Tokyo Night + Dracula Gruvbox Dark Claude Matrix @@ -17,7 +18,6 @@ - ● working ○ ready ✓ done ! cauti POD LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_120x32.txt b/crates/tui/src/tui/goldens/settings_120x32.txt index b396b406e1..80dd43c412 100644 --- a/crates/tui/src/tui/goldens/settings_120x32.txt +++ b/crates/tui/src/tui/goldens/settings_120x32.txt @@ -1,11 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Fleet Blue Stage ├── whale-1 · footer band - Work [ ✓ Blue Stage Light ] └── whale-2 · goldens - Tools & MCP Grayscale ● working whale-1 editing · 14:41:02 ×12 - Trust Catppuccin Mocha ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 - Motion Tokyo Night done: stage restyled - Advanced Dracula + Fleet Underwater ├── whale-1 · footer band + Work [ ✓ Blue Stage ] └── whale-2 · goldens + Tools & MCP Blue Stage Light ● working whale-1 editing · 14:41:02 ×12 + Trust Grayscale ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 + Motion Catppuccin Mocha done: stage restyled + Advanced Tokyo Night + Dracula Gruvbox Dark Claude Matrix @@ -19,7 +20,6 @@ - ● working ○ ready ✓ done ! caution ✗ f POD LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_160x40.txt b/crates/tui/src/tui/goldens/settings_160x40.txt index 937f3aa489..1de0161b46 100644 --- a/crates/tui/src/tui/goldens/settings_160x40.txt +++ b/crates/tui/src/tui/goldens/settings_160x40.txt @@ -1,11 +1,12 @@ ▸ Appearance System PREVIEW · Blue Stage Models & providers Terminal ▎ restyle the work stage - Fleet Blue Stage ├── whale-1 · footer band - Work [ ✓ Blue Stage Light ] └── whale-2 · goldens - Tools & MCP Grayscale ● working whale-1 editing · 14:41:02 ×12 - Trust Catppuccin Mocha ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 - Motion Tokyo Night done: stage restyled - Advanced Dracula + Fleet Underwater ├── whale-1 · footer band + Work [ ✓ Blue Stage ] └── whale-2 · goldens + Tools & MCP Blue Stage Light ● working whale-1 editing · 14:41:02 ×12 + Trust Grayscale ✓ done whale-2 surfaced ✓ · 14:39:02 ×34 + Motion Catppuccin Mocha done: stage restyled + Advanced Tokyo Night + Dracula Gruvbox Dark Claude Matrix @@ -27,7 +28,6 @@ - ● working ○ ready ✓ done ! caution ✗ failed POD LEDGER WHALE │ASSIGNMENT │STATE diff --git a/crates/tui/src/tui/goldens/settings_80x24.txt b/crates/tui/src/tui/goldens/settings_80x24.txt index cae106688d..0d1e55eb69 100644 --- a/crates/tui/src/tui/goldens/settings_80x24.txt +++ b/crates/tui/src/tui/goldens/settings_80x24.txt @@ -1,8 +1,9 @@ Appearance Models & providers Fleet Work Tools & MCP Trust › System Terminal - Blue Stage -[ ✓ Blue Stage Light ] + Underwater +[ ✓ Blue Stage ] + Blue Stage Light Grayscale Catppuccin Mocha Tokyo Night diff --git a/crates/tui/src/tui/goldens/startup_100x30.txt b/crates/tui/src/tui/goldens/startup_100x30.txt index a8009a494e..9ab0ae4a3e 100644 --- a/crates/tui/src/tui/goldens/startup_100x30.txt +++ b/crates/tui/src/tui/goldens/startup_100x30.txt @@ -8,9 +8,9 @@ ╭──────────────────────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ⣠⡾⠛⠷⠄ ● 2 MCP servers connected · 1 needs sign-in · run /mcp │ - │ ⣿⣄⣠⣤⣶⠶⡆ ▸ New worktree ctrl+n │ + │ ⣿⣄⣠⣤⣶⠶⡆ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/goldens/startup_120x32.txt b/crates/tui/src/tui/goldens/startup_120x32.txt index 6e062f51a9..7e001508e1 100644 --- a/crates/tui/src/tui/goldens/startup_120x32.txt +++ b/crates/tui/src/tui/goldens/startup_120x32.txt @@ -9,9 +9,9 @@ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ⣠⡾⠛⠷⠄ ● 2 MCP servers connected · 1 needs sign-in · run /mcp │ - │ ⣿⣄⣠⣤⣶⠶⡆ ▸ New worktree ctrl+n │ + │ ⣿⣄⣠⣤⣶⠶⡆ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/goldens/startup_160x40.txt b/crates/tui/src/tui/goldens/startup_160x40.txt index 5df7b14cf8..e870660a68 100644 --- a/crates/tui/src/tui/goldens/startup_160x40.txt +++ b/crates/tui/src/tui/goldens/startup_160x40.txt @@ -13,9 +13,9 @@ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ⣠⡾⠛⠷⠄ ● 2 MCP servers connected · 1 needs sign-in · run /mcp │ - │ ⣿⣄⣠⣤⣶⠶⡆ ▸ New worktree ctrl+n │ + │ ⣿⣄⣠⣤⣶⠶⡆ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/goldens/startup_40x10.txt b/crates/tui/src/tui/goldens/startup_40x10.txt index 19dbaaa93d..c29d91e536 100644 --- a/crates/tui/src/tui/goldens/startup_40x10.txt +++ b/crates/tui/src/tui/goldens/startup_40x10.txt @@ -1,8 +1,8 @@ ⑂ Hmbown/CodeWhale · main ╭──────────────────────────────╮ - │ ⢠⡞⠛⢂⣀ Codewhale │ + │ ⢠⡞⠛⢂⣀ codewhale │ │ ⠘⢿⣻⣟⠝ ● 2 MCP servers connec…│ - │ ▸ New worktree ctrl+n │ + │ New worktree ctrl+n │ ╰──────────────────────────────╯ ╭──────────────────────────────────────╮ │ ❯ ▌ │ diff --git a/crates/tui/src/tui/goldens/startup_80x24.txt b/crates/tui/src/tui/goldens/startup_80x24.txt index 8c4387b794..ca7582f358 100644 --- a/crates/tui/src/tui/goldens/startup_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_80x24.txt @@ -5,9 +5,9 @@ ╭──────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ⣠⡾⠛⠷⠄ ● 2 MCP servers connected · 1 needs sign-in · run /m…│ - │ ⣿⣄⣠⣤⣶⠶⡆ ▸ New worktree ctrl+n │ + │ ⣿⣄⣠⣤⣶⠶⡆ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/goldens/startup_first_run_80x24.txt b/crates/tui/src/tui/goldens/startup_first_run_80x24.txt index 21d2940dd8..17cc96561f 100644 --- a/crates/tui/src/tui/goldens/startup_first_run_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_first_run_80x24.txt @@ -5,9 +5,9 @@ ╭──────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ⣠⡾⠛⠷⠄ ⚠ no model connected · run /provider │ - │ ⣿⣄⣠⣤⣶⠶⡆ ▸ New worktree ctrl+n │ + │ ⣿⣄⣠⣤⣶⠶⡆ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/goldens/startup_ink_100x30.txt b/crates/tui/src/tui/goldens/startup_ink_100x30.txt index cff4c3e27a..b4b3a2d49c 100644 --- a/crates/tui/src/tui/goldens/startup_ink_100x30.txt +++ b/crates/tui/src/tui/goldens/startup_ink_100x30.txt @@ -10,10 +10,10 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb bbbbbbbbbbabbbbbbbbbcccccccccbaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbb bbbbbbbbbbabdddddbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebbbbbbbbbbbbbbbabbbbbbbbbb -bbbbbbbbbbabdddddddbfbggggggggggggbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbffffffbabbbbbbbbbb -bbbbbbbbbbabddddddbbabhhhhhhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb -bbbbbbbbbbabbbbbbbbbabhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb -bbbbbbbbbbabbbbbbbbbabhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb +bbbbbbbbbbabdddddddbabffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb +bbbbbbbbbbabddddddbbabffffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb +bbbbbbbbbbabbbbbbbbbabfffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb +bbbbbbbbbbabbbbbbbbbabffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbb bbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -24,18 +24,17 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii -ibibfbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbi -ibaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbjjjbi -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg +gbgbhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbg +gbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbiiibg +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg -- a #8A99B3 on reset b reset on reset c #F6C453 on reset BOLD d #F6C453 on reset e #B6C0D4 on reset -f #F6F2E8 on reset -g #F6F2E8 on reset BOLD -h #93A0B8 on reset -i #6AA6DC on reset -j #4FD1C5 on reset +f #93A0B8 on reset +g #6AA6DC on reset +h #F6F2E8 on reset +i #4FD1C5 on reset diff --git a/crates/tui/src/tui/goldens/startup_ink_120x32.txt b/crates/tui/src/tui/goldens/startup_ink_120x32.txt index 799151ea1a..9be8998879 100644 --- a/crates/tui/src/tui/goldens/startup_ink_120x32.txt +++ b/crates/tui/src/tui/goldens/startup_ink_120x32.txt @@ -11,10 +11,10 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbb bbbbbbbbbbbbabbbbbbbbbcccccccccbaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbbbb bbbbbbbbbbbbabdddddbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbbbb -bbbbbbbbbbbbabdddddddbfbggggggggggggbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbffffffbabbbbbbbbbbbb -bbbbbbbbbbbbabddddddbbabhhhhhhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb -bbbbbbbbbbbbabbbbbbbbbabhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb -bbbbbbbbbbbbabbbbbbbbbabhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabdddddddbabffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabddddddbbabffffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabbbbbbbbbabfffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabbbbbbbbbabffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb bbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -26,18 +26,17 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii -ibibfbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbi -ibaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbjjjbi -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg +gbgbhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbg +gbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbiiibg +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg -- a #8A99B3 on reset b reset on reset c #F6C453 on reset BOLD d #F6C453 on reset e #B6C0D4 on reset -f #F6F2E8 on reset -g #F6F2E8 on reset BOLD -h #93A0B8 on reset -i #6AA6DC on reset -j #4FD1C5 on reset +f #93A0B8 on reset +g #6AA6DC on reset +h #F6F2E8 on reset +i #4FD1C5 on reset diff --git a/crates/tui/src/tui/goldens/startup_ink_160x40.txt b/crates/tui/src/tui/goldens/startup_ink_160x40.txt index e453c1876d..973f292766 100644 --- a/crates/tui/src/tui/goldens/startup_ink_160x40.txt +++ b/crates/tui/src/tui/goldens/startup_ink_160x40.txt @@ -15,10 +15,10 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbb bbbbbbbbbbbbabbbbbbbbbcccccccccbaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbbbb bbbbbbbbbbbbabdddddbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeebbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbbbbbb -bbbbbbbbbbbbabdddddddbfbggggggggggggbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbffffffbabbbbbbbbbbbb -bbbbbbbbbbbbabddddddbbabhhhhhhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb -bbbbbbbbbbbbabbbbbbbbbabhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb -bbbbbbbbbbbbabbbbbbbbbabhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabdddddddbabffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabddddddbbabffffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabbbbbbbbbabfffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb +bbbbbbbbbbbbabbbbbbbbbabffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbbbbbb bbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -34,18 +34,17 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii -ibibfbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbi -ibaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbjjjbi -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg +gbgbhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbg +gbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbiiibg +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg -- a #8A99B3 on reset b reset on reset c #F6C453 on reset BOLD d #F6C453 on reset e #B6C0D4 on reset -f #F6F2E8 on reset -g #F6F2E8 on reset BOLD -h #93A0B8 on reset -i #6AA6DC on reset -j #4FD1C5 on reset +f #93A0B8 on reset +g #6AA6DC on reset +h #F6F2E8 on reset +i #4FD1C5 on reset diff --git a/crates/tui/src/tui/goldens/startup_ink_80x24.txt b/crates/tui/src/tui/goldens/startup_ink_80x24.txt index 90feb11852..d72a15efed 100644 --- a/crates/tui/src/tui/goldens/startup_ink_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_ink_80x24.txt @@ -7,10 +7,10 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb bbbbbbbbabbbbbbbbbcccccccccbaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbabbbbbbbb bbbbbbbbabdddddbbbeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeabbbbbbbb -bbbbbbbbabdddddddbfbggggggggggggbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbffffffbabbbbbbbb -bbbbbbbbabddddddbbabhhhhhhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb -bbbbbbbbabbbbbbbbbabhhhhhhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb -bbbbbbbbabbbbbbbbbabhhhhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb +bbbbbbbbabdddddddbabffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb +bbbbbbbbabddddddbbabffffffffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb +bbbbbbbbabbbbbbbbbabfffffffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb +bbbbbbbbabbbbbbbbbabffffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaababbbbbbbb bbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -18,18 +18,17 @@ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii -ibibfbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbi -ibaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbjjjbi -iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg +gbgbhbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbg +gbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbiiibg +gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg -- a #8A99B3 on reset b reset on reset c #F6C453 on reset BOLD d #F6C453 on reset e #B6C0D4 on reset -f #F6F2E8 on reset -g #F6F2E8 on reset BOLD -h #93A0B8 on reset -i #6AA6DC on reset -j #4FD1C5 on reset +f #93A0B8 on reset +g #6AA6DC on reset +h #F6F2E8 on reset +i #4FD1C5 on reset diff --git a/crates/tui/src/tui/goldens/startup_surfacing_80x24.txt b/crates/tui/src/tui/goldens/startup_surfacing_80x24.txt index 072815c456..b40fdef651 100644 --- a/crates/tui/src/tui/goldens/startup_surfacing_80x24.txt +++ b/crates/tui/src/tui/goldens/startup_surfacing_80x24.txt @@ -5,9 +5,9 @@ ╭──────────────────────────────────────────────────────────────╮ - │ Codewhale v0.9.12 │ + │ codewhale v0.9.12 │ │ ● 2 MCP servers connected · 1 needs sign-in · run /m…│ - │ ⣤⣄⣠⣤⣤⠤⡄ ▸ New worktree ctrl+n │ + │ ⣤⣄⣠⣤⣤⠤⡄ New worktree ctrl+n │ │ ⠙⠿⣯⣿⡡⠞ Resume session ctrl+r │ │ Changelog ctrl+l │ │ Quit ctrl+q │ diff --git a/crates/tui/src/tui/infoline.rs b/crates/tui/src/tui/infoline.rs index 8f33068059..85a42eb1c8 100644 --- a/crates/tui/src/tui/infoline.rs +++ b/crates/tui/src/tui/infoline.rs @@ -72,6 +72,8 @@ pub enum InfoSegmentId { Ttft, /// Output rate (`38 tok/s`). Rate, + /// Prompt cache hit percent (`cache 85%`). + Cache, } impl InfoSegmentId { @@ -83,6 +85,7 @@ impl InfoSegmentId { pub fn shed_priority(self) -> u8 { match self { Self::Rate => 9, + Self::Cache => 8, Self::Ttft => 8, Self::OutputTokens => 7, Self::Cost => 6, diff --git a/crates/tui/src/tui/notification_payload.rs b/crates/tui/src/tui/notification_payload.rs index 9fb0a21289..25d839cf6e 100644 --- a/crates/tui/src/tui/notification_payload.rs +++ b/crates/tui/src/tui/notification_payload.rs @@ -66,7 +66,7 @@ pub const REDACTED: &str = "[redacted]"; pub const HIDDEN_DETAILS: &str = "[details hidden]"; /// Fallback headline when sanitization leaves nothing behind. -const FALLBACK_HEADLINE: &str = "Codewhale"; +const FALLBACK_HEADLINE: &str = "codewhale"; /// The closed set of events that can produce a desktop notification. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/tui/src/tui/notifications.rs b/crates/tui/src/tui/notifications.rs index f4f3d51750..4d02762588 100644 --- a/crates/tui/src/tui/notifications.rs +++ b/crates/tui/src/tui/notifications.rs @@ -699,7 +699,7 @@ pub fn set_title_prefix(prefix: Option<&str>) { if TITLE_ANIMATION_RUNNING.load(Ordering::SeqCst) { let base = title_animation_base() .lock() - .map_or_else(|_| "Codewhale".to_string(), |base| base.clone()); + .map_or_else(|_| "codewhale".to_string(), |base| base.clone()); let motion = TITLE_MOTION_ENABLED.load(Ordering::SeqCst); set_terminal_title(&title_activity_label( &base, @@ -722,7 +722,7 @@ fn resting_title_body() -> &'static str { if COMPLETION_MARKER_SHOWN.load(Ordering::SeqCst) { "✓ done" } else { - "Codewhale" + "codewhale" } } @@ -746,7 +746,7 @@ const TITLE_FRAME_HOLD: Duration = Duration::from_millis(800); const TITLE_WHALE_FRAMES: &[&str] = &["🐳", "🐋", "🐳", "🐋"]; fn title_animation_base() -> &'static Mutex { - TITLE_ANIMATION_BASE.get_or_init(|| Mutex::new("Codewhale".to_string())) + TITLE_ANIMATION_BASE.get_or_init(|| Mutex::new("codewhale".to_string())) } fn title_activity_verb() -> &'static Mutex { @@ -779,7 +779,7 @@ pub fn set_title_activity_verb(verb: &str) { } let base = title_animation_base() .lock() - .map_or_else(|_| "Codewhale".to_string(), |base| base.clone()); + .map_or_else(|_| "codewhale".to_string(), |base| base.clone()); set_terminal_title(&title_activity_label( &base, Duration::ZERO, @@ -911,7 +911,7 @@ pub fn set_terminal_focused(focused: bool) { } let base = title_animation_base() .lock() - .map_or_else(|_| "Codewhale".to_string(), |base| base.clone()); + .map_or_else(|_| "codewhale".to_string(), |base| base.clone()); let motion = TITLE_MOTION_ENABLED.load(Ordering::SeqCst); set_terminal_title(&title_activity_label( &base, @@ -946,7 +946,7 @@ pub fn stop_title_animation_quietly() { TITLE_ANIMATION_RUNNING.store(false, Ordering::SeqCst); TITLE_ANIMATION_GENERATION.fetch_add(1, Ordering::SeqCst); COMPLETION_MARKER_SHOWN.store(false, Ordering::SeqCst); - set_terminal_title(&decorate_title("Codewhale")); + set_terminal_title(&decorate_title("codewhale")); } /// Clear the completion marker from the title when the user interacts. @@ -955,7 +955,7 @@ pub fn stop_title_animation_quietly() { /// marker doesn't persist once the user is back at the terminal. pub fn reset_title_on_interaction() { if COMPLETION_MARKER_SHOWN.swap(false, Ordering::SeqCst) { - set_terminal_title(&decorate_title("Codewhale")); + set_terminal_title(&decorate_title("codewhale")); } } @@ -1423,19 +1423,19 @@ mod tests { "in the current…".clone_into(&mut *verb); } assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, true, true), + title_activity_label("codewhale", Duration::ZERO, true, true), "🐳 in the current…" ); assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, false, false), + title_activity_label("codewhale", Duration::ZERO, false, false), "🐳 in the current…" ); assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, false, true), + title_activity_label("codewhale", Duration::ZERO, false, true), "🐳 in the current…" ); assert_eq!( - title_activity_label("Codewhale", Duration::from_millis(800), false, true), + title_activity_label("codewhale", Duration::from_millis(800), false, true), "🐋 in the current…" ); } @@ -1460,16 +1460,16 @@ mod tests { "reasoning…".clone_into(&mut *verb); } assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, true, true), + title_activity_label("codewhale", Duration::ZERO, true, true), "[task-7] 🐳 reasoning…" ); assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, false, true), + title_activity_label("codewhale", Duration::ZERO, false, true), "[task-7] 🐳 reasoning…" ); set_title_prefix(None); assert_eq!( - title_activity_label("Codewhale", Duration::ZERO, true, true), + title_activity_label("codewhale", Duration::ZERO, true, true), "🐳 reasoning…" ); } @@ -1478,14 +1478,14 @@ mod tests { fn title_prefix_decorates_rest_and_completion_titles() { let _guard = prefix_lock(); set_title_prefix(Some("feature/x")); - assert_eq!(decorate_title("Codewhale"), "[feature/x] Codewhale"); + assert_eq!(decorate_title("codewhale"), "[feature/x] codewhale"); assert_eq!(decorate_title("✓ done"), "[feature/x] ✓ done"); set_title_prefix(None); - assert_eq!(decorate_title("Codewhale"), "Codewhale"); + assert_eq!(decorate_title("codewhale"), "codewhale"); assert_eq!(decorate_title("✓ done"), "✓ done"); // Empty/whitespace prefixes behave exactly like `None`. set_title_prefix(Some(" ")); - assert_eq!(decorate_title("Codewhale"), "Codewhale"); + assert_eq!(decorate_title("codewhale"), "codewhale"); set_title_prefix(None); } @@ -1512,7 +1512,7 @@ mod tests { // whole render loop. Exercise the exact path: prefix change while // the animation worker is running. let _guard = prefix_lock(); - start_title_animation("Codewhale"); + start_title_animation("codewhale"); assert!(TITLE_ANIMATION_RUNNING.load(Ordering::SeqCst)); set_title_prefix(Some("task-7")); assert_eq!(title_prefix_slot().lock().unwrap().as_str(), "task-7"); @@ -1900,12 +1900,12 @@ mod tests { TITLE_ANIMATION_RUNNING.store(false, Ordering::SeqCst); COMPLETION_MARKER_SHOWN.store(false, Ordering::SeqCst); set_title_prefix(Some("Alpha")); - assert_eq!(decorate_title(resting_title_body()), "[Alpha] Codewhale"); + assert_eq!(decorate_title(resting_title_body()), "[Alpha] codewhale"); COMPLETION_MARKER_SHOWN.store(true, Ordering::SeqCst); assert_eq!(decorate_title(resting_title_body()), "[Alpha] ✓ done"); COMPLETION_MARKER_SHOWN.store(false, Ordering::SeqCst); set_title_prefix(None); - assert_eq!(decorate_title(resting_title_body()), "Codewhale"); + assert_eq!(decorate_title(resting_title_body()), "codewhale"); } #[test] diff --git a/crates/tui/src/tui/ocean.rs b/crates/tui/src/tui/ocean.rs index 02f8917c58..bd03f1aaf1 100644 --- a/crates/tui/src/tui/ocean.rs +++ b/crates/tui/src/tui/ocean.rs @@ -2,55 +2,18 @@ //! //! The field is atmosphere, never content: ordinary shell cells share its //! water column while semantic surfaces such as selections, errors, and code -//! keep their own backgrounds. It is an explicit treatment, rather than a -//! layer forced onto every terminal. +//! keep their own backgrounds. It belongs to the `underwater` theme alone +//! (`ThemeId::Underwater`); every other theme leaves the terminal's ground +//! untouched. Motion inside the field remains governed separately by +//! `low_motion`/`fancy_animations`. use ratatui::{buffer::Buffer, layout::Rect, style::Color}; -use crate::palette::{PaletteMode, UiTheme}; +use crate::palette::UiTheme; use crate::tui::underwater::ShellPhase; -/// Appearance treatment for the underwater shell. -/// -/// Parsed once from persisted settings so rendering and scheduling code can -/// branch on typed state instead of scattered string comparisons. `Deepsea` is -/// the opt-in underwater scene; `Flat` leaves the host/theme surface alone. -/// Motion inside the selected scene remains governed separately by -/// `low_motion`/`fancy_animations`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum OceanTreatment { - /// State-reactive water column painted from the theme's [`OceanRamp`]. - Deepsea, - /// Plain theme surface with no atmospheric treatment. This is the - /// terminal-respecting default; the host owns the background. - #[default] - Flat, -} - -impl OceanTreatment { - #[must_use] - pub fn parse(value: &str) -> Self { - match value.trim().to_ascii_lowercase().as_str() { - "deepsea" | "underwater" | "ombre" | "gradient" | "classic" => Self::Deepsea, - // Invalid persisted values must never opt the user into a painted - // surface. The settings editor validates new values before save. - _ => Self::Flat, - } - } - - #[must_use] - pub fn is_deepsea(self) -> bool { - self == Self::Deepsea - } - - #[must_use] - pub fn is_flat(self) -> bool { - self == Self::Flat - } -} - /// Minimum empty-water size that earns decorative ambient life when the -/// underwater treatment is selected. Below this, content and controls own +/// underwater theme is selected. Below this, content and controls own /// every cell. Shared by the renderer and idle animation scheduler so redraws /// are never scheduled for invisible life. pub const AMBIENT_MIN_WIDTH: u16 = 40; @@ -78,9 +41,9 @@ pub fn ambient_inks_for_activity( AmbientActivity::Subagents => (0.34, 0.22), AmbientActivity::Verifying | AmbientActivity::Baseline => (0.42, 0.28), }; - // The built-in Whale pair deliberately leaves its Flat shell at Reset. - // When Deepsea is selected, use the authored column as the color-mixing - // base so its ambient life retains depth and activity-specific inks. + // Only the underwater theme owns a painted base column; everywhere else + // the terminal's own ground (Color::Reset) is the base and the inks fall + // back to the theme's info lane. let mix_base = rgb(theme.surface_bg) .or_else(|| OceanRamp::for_theme(theme).and_then(|ramp| rgb(ramp.middle))); match mix_base { @@ -182,6 +145,7 @@ pub struct OceanColumn { animated: bool, /// Fixed-point (0..=1000) life presence; keeps `Eq` derivable. presence: u16, + context_percent: u8, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -193,6 +157,7 @@ struct OceanRampCacheIdentity { animated: bool, completion_active: bool, presence: u16, + context_percent: u8, } impl OceanRampCacheIdentity { @@ -213,6 +178,7 @@ impl OceanRampCacheIdentity { u32::from(self.animated), u32::from(self.completion_active), u32::from(self.presence), + u32::from(self.context_percent), ] .into_iter() .flat_map(u32::to_le_bytes) @@ -256,6 +222,7 @@ impl OceanColumn { phase: ShellPhase, animated: bool, presence: u16, + context_percent: u8, ) -> Self { Self { ramp, @@ -266,6 +233,7 @@ impl OceanColumn { phase, animated, presence, + context_percent: context_percent.min(100), } } @@ -273,7 +241,8 @@ impl OceanColumn { pub fn color_at_y(self, y: u16) -> Color { let row = y.saturating_sub(self.top).min(self.height - 1); if let Some(elapsed) = self.completion_elapsed_ms { - self.ramp.color_at_completion(row, self.height, elapsed) + self.ramp + .color_at_completion_context(row, self.height, elapsed, self.context_percent) } else { // Attention states tint the water itself, independent of life // presence: a session blocked on approval or ended in failure @@ -284,15 +253,26 @@ impl OceanColumn { self.phase, ShellPhase::Waiting | ShellPhase::Approval | ShellPhase::Failed ) { - return self.ramp.color_at_attention(row, self.height, self.phase); + return self.ramp.color_at_attention_context( + row, + self.height, + self.phase, + self.context_percent, + ); } // Ease between the static gradient and the phase treatment by // life presence, so mood/activity changes blend instead of snap. - let static_color = self.ramp.color_at(row, self.height); + let static_color = self + .ramp + .color_at_context(row, self.height, self.context_percent); if self.animated || self.presence > 0 { - let phase_color = - self.ramp - .color_at_phase(row, self.height, self.elapsed_ms, self.phase); + let phase_color = self.ramp.color_at_phase_context( + row, + self.height, + self.elapsed_ms, + self.phase, + self.context_percent, + ); mix_colors(static_color, phase_color, self.presence_f32()) } else { static_color @@ -337,6 +317,7 @@ impl OceanColumn { animated: self.animated, completion_active: self.completion_elapsed_ms.is_some(), presence: self.presence, + context_percent: self.context_percent, } } @@ -373,110 +354,69 @@ impl OceanColumn { impl OceanRamp { #[must_use] pub fn for_theme(theme: &UiTheme) -> Option { - // Solarized Light's canonical Base3 (#fdf6e3) background is part of - // the named palette's contract. Tinting it with the underwater field - // turns the shell green-grey and no longer renders Solarized Light - // (#4457). A non-canonical user-supplied background is a separate - // contract and must keep the configured Deepsea treatment. - if theme.mode == PaletteMode::SolarizedLight - && theme.surface_bg == crate::palette::SOLARIZED_LIGHT_UI_THEME.surface_bg - { + // The painted field exists only under the underwater theme; every + // other theme leaves the terminal's ground alone. A user-supplied + // `background_color` rewrites the underwater surfaces through + // `with_background_color` and remains the source of truth there. + if theme.name != crate::palette::UNDERWATER_UI_THEME.name { return None; } - // The canonical Whale pair gets the authored Codewhale water column. - // Match both name and surface so a user-supplied `background_color` - // remains the source of truth and still receives the generic ramp. - if theme.name == crate::palette::UI_THEME.name - && theme.surface_bg == crate::palette::UI_THEME.surface_bg - { - return Some(Self { - // Keep the authored Whale column unmistakably blue all the - // way to the floor. These restrained ocean shades sit between - // the shell's ink surfaces and its ambient blue: the empty - // field gains depth without becoming a saturated blue panel. - surface: Color::Rgb(0x10, 0x2a, 0x45), - middle: Color::Rgb(0x0a, 0x1e, 0x33), - deep: Color::Rgb(0x06, 0x13, 0x20), - ambient: Color::Rgb(0x26, 0x48, 0x66), - attention: theme.warning, - failure: theme.error_fg, - }); - } - if theme.name == crate::palette::LIGHT_UI_THEME.name - && theme.surface_bg == crate::palette::LIGHT_UI_THEME.surface_bg - { - return Some(Self { - surface: Color::Rgb(0xff, 0xfd, 0xf8), - middle: Color::Rgb(0xf4, 0xf7, 0xfb), - deep: Color::Rgb(0xf0, 0xf4, 0xf9), - ambient: Color::Rgb(0x9a, 0xb8, 0xe0), - attention: theme.warning, - failure: theme.error_fg, - }); - } - - let base = rgb(theme.surface_bg)?; - let seafoam = rgb(theme.accent_secondary).unwrap_or((79, 209, 197)); - - let (surface, middle, deep) = match theme.mode { - PaletteMode::Light | PaletteMode::SolarizedLight => ( - mix(base, seafoam, 0.07), - mix(base, seafoam, 0.13), - mix(base, (70, 139, 196), 0.18), - ), - PaletteMode::Dark | PaletteMode::Grayscale => ( - mix(base, (30, 71, 103), 0.24), - mix(base, (7, 30, 54), 0.40), - mix(base, (2, 9, 24), 0.64), - ), - }; - Some(Self { - surface: color(surface), - middle: color(middle), - deep: color(deep), - ambient: color(mix(seafoam, base, 0.42)), + // The authored Codewhale water column: unmistakably blue all the + // way to the floor. These restrained ocean shades sit between the + // shell's ink surfaces and its ambient blue, so the field gains + // depth without becoming a saturated blue panel. + surface: Color::Rgb(0x10, 0x2a, 0x45), + middle: Color::Rgb(0x0a, 0x1e, 0x33), + deep: Color::Rgb(0x06, 0x13, 0x20), + ambient: Color::Rgb(0x26, 0x48, 0x66), attention: theme.warning, failure: theme.error_fg, }) } + /// Abyss Depth effect: wires context fullness (0..=100) into the water + /// column gradient calculation so that as context fills up, the dark + /// abyssal deep rises up to consume the sunlit surface gradient. #[must_use] - pub fn color_at(self, row: u16, height: u16) -> Color { + pub fn color_at_context(self, row: u16, height: u16, context_percent: u8) -> Color { if height <= 1 { - return self.surface; + let abyss = f32::from(context_percent.min(100)) / 100.0; + return mix_colors(self.surface, self.deep, abyss); } - let position = f32::from(row.min(height - 1)) / f32::from(height - 1); + let base_position = f32::from(row.min(height - 1)) / f32::from(height - 1); + let abyss_rise = f32::from(context_percent.min(100)) / 100.0; + let position = (base_position + abyss_rise).min(1.0); // One continuous darkening curve (quadratic Bézier through - // surface → middle → deep, via de Casteljau). The former two eased - // segments met at a 0.42 anchor where the color velocity dropped to - // zero on both sides — on a tall window that shelf of unchanging - // middle color read as a horizontal seam across the water. + // surface → middle → deep, via de Casteljau). let toward_middle = mix_colors(self.surface, self.middle, position); let toward_deep = mix_colors(self.middle, self.deep, position); mix_colors(toward_middle, toward_deep, position) } #[must_use] - pub fn color_at_phase( + pub fn color_at_phase_context( self, row: u16, height: u16, elapsed_ms: u128, phase: ShellPhase, + context_percent: u8, ) -> Color { - let base = self.color_at(row, height); + let base = self.color_at_context(row, height, context_percent); let depth = if height <= 1 { 0.0 } else { - f32::from(row.min(height - 1)) / f32::from(height - 1) + let base_depth = f32::from(row.min(height - 1)) / f32::from(height - 1); + let abyss_rise = f32::from(context_percent.min(100)) / 100.0; + (base_depth + abyss_rise).min(1.0) }; if matches!( phase, ShellPhase::Waiting | ShellPhase::Approval | ShellPhase::Failed ) { - return self.color_at_attention(row, height, phase); + return self.color_at_attention_context(row, height, phase, context_percent); } let cycle = (elapsed_ms % 90_000) as f32 / 90_000.0; let breath = (cycle * std::f32::consts::TAU).sin() * 0.5 + 0.5; @@ -492,18 +432,21 @@ impl OceanRamp { } /// Water tint for the states that need to read from across the room. - /// - /// Waiting/Approval warm the field toward `attention`, concentrated near - /// the surface where the eye lands first; Failed casts a steady `failure` - /// tone. Deliberately time-invariant: the color itself is the signal, and - /// a slow breath read as flicker rather than intent. #[must_use] - pub fn color_at_attention(self, row: u16, height: u16, phase: ShellPhase) -> Color { - let base = self.color_at(row, height); + pub fn color_at_attention_context( + self, + row: u16, + height: u16, + phase: ShellPhase, + context_percent: u8, + ) -> Color { + let base = self.color_at_context(row, height, context_percent); let depth = if height <= 1 { 0.0 } else { - f32::from(row.min(height - 1)) / f32::from(height - 1) + let base_depth = f32::from(row.min(height - 1)) / f32::from(height - 1); + let abyss_rise = f32::from(context_percent.min(100)) / 100.0; + (base_depth + abyss_rise).min(1.0) }; match phase { ShellPhase::Waiting | ShellPhase::Approval => { @@ -515,8 +458,14 @@ impl OceanRamp { } #[must_use] - pub fn color_at_completion(self, row: u16, height: u16, elapsed_ms: u128) -> Color { - let base = self.color_at(row, height); + pub fn color_at_completion_context( + self, + row: u16, + height: u16, + elapsed_ms: u128, + context_percent: u8, + ) -> Color { + let base = self.color_at_context(row, height, context_percent); let elapsed = elapsed_ms.min(800) as f32 / 800.0; let brightness = if elapsed <= 0.4 { 0.88 + (1.12 - 0.88) * (elapsed / 0.4) diff --git a/crates/tui/src/tui/ocean/tests.rs b/crates/tui/src/tui/ocean/tests.rs index fc7730704c..503395a510 100644 --- a/crates/tui/src/tui/ocean/tests.rs +++ b/crates/tui/src/tui/ocean/tests.rs @@ -32,7 +32,7 @@ fn contrast_ratio(foreground: Color, background: Color) -> f64 { #[test] fn whale_ramp_is_perceptibly_deep_not_merely_non_equal() { - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); assert_eq!(ramp.surface, Color::Rgb(0x10, 0x2a, 0x45)); assert_eq!(ramp.middle, Color::Rgb(0x0a, 0x1e, 0x33)); assert_eq!(ramp.deep, Color::Rgb(0x06, 0x13, 0x20)); @@ -40,17 +40,20 @@ fn whale_ramp_is_perceptibly_deep_not_merely_non_equal() { distance(ramp.surface, ramp.deep) >= 32, "the selected underwater treatment must read at a glance" ); - assert_ne!(ramp.color_at(0, 20), ramp.color_at(19, 20)); + assert_ne!( + ramp.color_at_context(0, 20, 0), + ramp.color_at_context(19, 20, 0) + ); } #[test] fn whale_column_stays_blue_and_gently_banded_at_full_screen_depth() { - let theme = crate::palette::UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); - let mut previous = ramp.color_at(0, 80); + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); + let mut previous = ramp.color_at_context(0, 80, 0); for row in 0..80 { - let current = ramp.color_at(row, 80); + let current = ramp.color_at_context(row, 80, 0); let (red, green, blue) = rgb(current).expect("RGB ocean color"); assert!( blue > green && green > red, @@ -67,14 +70,14 @@ fn whale_column_stays_blue_and_gently_banded_at_full_screen_depth() { previous = current; } - assert_eq!(ramp.color_at(0, 80), ramp.surface); - assert_eq!(ramp.color_at(79, 80), ramp.deep); + assert_eq!(ramp.color_at_context(0, 80, 0), ramp.surface); + assert_eq!(ramp.color_at_context(79, 80, 0), ramp.deep); } #[test] fn whale_ocean_keeps_text_and_semantic_roles_readable() { - let theme = crate::palette::UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); let foregrounds = [ ("body", theme.text_body), ("soft", theme.text_soft), @@ -108,62 +111,39 @@ fn whale_ocean_keeps_text_and_semantic_roles_readable() { } #[test] -fn light_theme_stays_light_enough_for_light_theme_text() { - let ramp = OceanRamp::for_theme(&crate::palette::LIGHT_UI_THEME).expect("RGB theme"); - assert_eq!(ramp.surface, Color::Rgb(0xff, 0xfd, 0xf8)); - assert_eq!(ramp.middle, Color::Rgb(0xf4, 0xf7, 0xfb)); - assert_eq!(ramp.deep, Color::Rgb(0xf0, 0xf4, 0xf9)); - let (r, g, b) = rgb(ramp.deep).expect("RGB color"); - assert!(u16::from(r) + u16::from(g) + u16::from(b) > 420); +fn underwater_custom_background_keeps_the_field() { + let custom = Color::Rgb(0x12, 0x1a, 0x2d); + let theme = crate::palette::UNDERWATER_UI_THEME.with_background_color(custom); + let ramp = OceanRamp::for_theme(&theme).expect("the field survives a background override"); + + assert_ne!(ramp.surface, ramp.deep); } #[test] -fn light_ocean_and_selection_keep_text_and_semantic_roles_readable() { - let theme = crate::palette::LIGHT_UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); - let foregrounds = [ - ("body", theme.text_body), - ("soft", theme.text_soft), - ("muted", theme.text_muted), - ("hint", theme.text_hint), - ("action", theme.accent_primary), - ("live", theme.status_working), - ("human", theme.accent_action), - ("warning", theme.warning), - ("danger", theme.error_fg), - ("act mode", theme.mode_agent), - ("plan mode", theme.mode_plan), - ("operate", theme.mode_operate), - ("full-access mode", theme.mode_yolo), - ("success", theme.success), - ("user", crate::palette::LIGHT_USER_BODY), - ]; - let backgrounds = [ - ("ocean surface", ramp.surface), - ("ocean middle", ramp.middle), - ("ocean deep", ramp.deep), - ("selection", theme.selection_bg), - ]; +fn themes_other_than_underwater_own_no_field() { + use crate::palette::{SELECTABLE_THEMES, ThemeId}; - for (background_name, background) in backgrounds { - for (foreground_name, foreground) in foregrounds { - let ratio = contrast_ratio(foreground, background); - assert!( - ratio >= 4.5, - "light {foreground_name} on {background_name} contrast {ratio:.2} is below 4.50" + for id in SELECTABLE_THEMES { + let ramp = OceanRamp::for_theme(&id.ui_theme()); + if matches!(id, ThemeId::Underwater) { + assert!(ramp.is_some(), "{} owns the painted field", id.name()); + } else { + assert_eq!( + ramp, + None, + "{} must leave the terminal's ground alone", + id.name() ); } } -} -#[test] -fn whale_custom_background_uses_the_configured_surface() { + // A custom background repaints surfaces; it never grants a field to a + // theme that does not own one. let custom = Color::Rgb(0x12, 0x1a, 0x2d); - let theme = crate::palette::UI_THEME.with_background_color(custom); - let ramp = OceanRamp::for_theme(&theme).expect("custom backgrounds retain Deepsea"); - - assert_ne!(ramp.surface, Color::Rgb(0x0e, 0x17, 0x29)); - assert_ne!(ramp.surface, ramp.deep); + assert_eq!( + OceanRamp::for_theme(&crate::palette::UI_THEME.with_background_color(custom)), + None + ); } #[test] @@ -183,70 +163,22 @@ fn solarized_light_preserves_its_canonical_base3_background() { } #[test] -fn solarized_light_custom_background_preserves_deepsea() { +fn solarized_light_custom_background_stays_field_free() { let custom = Color::Rgb(0x1a, 0x1b, 0x26); let theme = crate::palette::SOLARIZED_LIGHT_UI_THEME.with_background_color(custom); - let ramp = OceanRamp::for_theme(&theme).expect("custom backgrounds retain Deepsea"); - - assert_ne!(ramp.surface, custom); - assert_ne!(ramp.surface, ramp.deep); -} - -#[test] -fn every_shipped_theme_has_an_intentional_ocean_treatment() { - use crate::palette::{SELECTABLE_THEMES, ThemeId}; - - for id in SELECTABLE_THEMES { - let ramp = OceanRamp::for_theme(&id.ui_theme()); - if matches!(id, ThemeId::Terminal | ThemeId::SolarizedLight) { - assert_eq!( - ramp, - None, - "{} must keep its canonical background", - id.name() - ); - } else { - let ramp = ramp.unwrap_or_else(|| panic!("{} has no ocean ramp", id.name())); - assert_ne!( - ramp.surface, - ramp.deep, - "{} lost underwater depth", - id.name() - ); - } - } -} - -#[test] -fn treatment_parses_saved_values_and_migrates_legacy_ombre_values() { - assert_eq!(OceanTreatment::parse("flat"), OceanTreatment::Flat); - assert_eq!(OceanTreatment::parse(" FLAT "), OceanTreatment::Flat); - assert_eq!(OceanTreatment::parse("deepsea"), OceanTreatment::Deepsea); - assert_eq!(OceanTreatment::parse("ombre"), OceanTreatment::Deepsea); - assert_eq!(OceanTreatment::parse("underwater"), OceanTreatment::Deepsea); - assert_eq!(OceanTreatment::parse("kelp"), OceanTreatment::Flat); - assert_eq!(OceanTreatment::parse(""), OceanTreatment::Flat); - // Migration aliases remain deterministic for older persisted settings. - assert_eq!(OceanTreatment::parse("classic"), OceanTreatment::Deepsea); -} - -#[test] -fn deepsea_is_the_explicit_underwater_treatment() { - // The ordinary terminal must not be turned into an aquarium by default. - // Flat and Deepsea stay distinct so the user can deliberately opt into the - // underwater field. - assert_eq!(OceanTreatment::default(), OceanTreatment::Flat); - assert_ne!(OceanTreatment::Deepsea, OceanTreatment::Flat); - assert!(OceanTreatment::Deepsea.is_deepsea()); - assert!(OceanTreatment::Flat.is_flat()); + assert_eq!(OceanRamp::for_theme(&theme), None); + assert_eq!(theme.surface_bg, custom); } #[test] -fn whale_pair_flat_shells_reset_while_deepsea_paints_the_shared_column() { - assert!(OceanTreatment::Flat.is_flat()); - assert!(OceanTreatment::Deepsea.is_deepsea()); - +fn terminal_native_themes_keep_reset_shells_while_underwater_paints_the_column() { for theme in [crate::palette::UI_THEME, crate::palette::LIGHT_UI_THEME] { + assert_eq!( + OceanRamp::for_theme(&theme), + None, + "{} must not grow a field", + theme.name + ); for shell_surface in [ theme.surface_bg, theme.panel_bg, @@ -254,30 +186,38 @@ fn whale_pair_flat_shells_reset_while_deepsea_paints_the_shared_column() { theme.header_bg, theme.footer_bg, ] { - assert_eq!(shell_surface, Color::Reset, "{} Flat shell", theme.name); + assert_eq!(shell_surface, Color::Reset, "{} shell", theme.name); } + } - let ramp = OceanRamp::for_theme(&theme).expect("built-in Deepsea ramp"); - for painted in [ramp.surface, ramp.middle, ramp.deep, ramp.ambient] { - assert_ne!(painted, Color::Reset, "{} Deepsea paint", theme.name); - } + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); + for painted in [ramp.surface, ramp.middle, ramp.deep, ramp.ambient] { + assert_ne!(painted, Color::Reset, "underwater paint"); + } - let area = Rect::new(0, 0, 4, 4); - let mut buf = Buffer::empty(area); - let column = OceanColumn::new(ramp, area, 0, None, ShellPhase::Idle, false, 0); - column.paint_matching(area, &mut buf, theme.surface_bg); - assert_ne!(buf[(0, 0)].bg, Color::Reset); - assert_ne!(buf[(0, area.height - 1)].bg, Color::Reset); - assert_ne!(buf[(0, 0)].bg, buf[(0, area.height - 1)].bg); + let area = Rect::new(0, 0, 4, 4); + let mut buf = Buffer::empty(area); + // The shell has already painted its surface; paint_matching only re-inks + // cells wearing that exact background. + for y in 0..area.height { + for x in 0..area.width { + buf[(x, y)].set_bg(theme.surface_bg); + } } + let column = OceanColumn::new(ramp, area, 0, None, ShellPhase::Idle, false, 0, 0); + column.paint_matching(area, &mut buf, theme.surface_bg); + assert_ne!(buf[(0, 0)].bg, Color::Reset); + assert_ne!(buf[(0, area.height - 1)].bg, Color::Reset); + assert_ne!(buf[(0, 0)].bg, buf[(0, area.height - 1)].bg); } #[test] fn ambient_ink_matches_sunk_sky_shades_and_survives_reset_surfaces() { - // Deepsea's authored RGB ramp gives the terminal-native Whale shell two - // sunk sky shades; seafoam remains live-work ink. - let theme = crate::palette::UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); + // The underwater theme's authored RGB ramp gives its shell two sunk sky + // shades; seafoam remains live-work ink. + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); let baseline = crate::tui::ambient_life::AmbientActivity::Baseline; let (primary, secondary) = ambient_inks_for_activity(&theme, baseline); assert_ne!(primary, ramp.ambient); @@ -296,7 +236,7 @@ fn ambient_ink_matches_sunk_sky_shades_and_survives_reset_surfaces() { #[test] fn ambient_ink_reads_the_activity_at_a_glance() { use crate::tui::ambient_life::AmbientActivity; - let theme = crate::palette::UI_THEME; + let theme = crate::palette::UNDERWATER_UI_THEME; let baseline = ambient_inks_for_activity(&theme, AmbientActivity::Baseline); let reasoning = ambient_inks_for_activity(&theme, AmbientActivity::Reasoning); let tools = ambient_inks_for_activity(&theme, AmbientActivity::Tools); @@ -318,30 +258,41 @@ fn ambient_ink_reads_the_activity_at_a_glance() { #[test] fn attention_phases_tint_the_water_even_when_life_has_settled() { let viewport = Rect::new(0, 0, 80, 24); - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); // presence 0 + animated false is the fully settled, reduced-motion case — // exactly where the old treatment went neutral and a blocked session was // indistinguishable from an idle one across the room. - let waiting = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Approval, false, 0); - let failed = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Failed, false, 0); - let idle = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0); + let waiting = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Approval, false, 0, 0); + let failed = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Failed, false, 0, 0); + let idle = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0, 0); assert_ne!(waiting.color_at_y(0), idle.color_at_y(0)); assert_ne!(failed.color_at_y(0), idle.color_at_y(0)); assert_ne!(waiting.color_at_y(0), failed.color_at_y(0)); // The tint is steady across time and motion settings alike. - let later = OceanColumn::new(ramp, viewport, 700, None, ShellPhase::Approval, true, 0); + let later = OceanColumn::new(ramp, viewport, 700, None, ShellPhase::Approval, true, 0, 0); assert_eq!(waiting.color_at_y(0), later.color_at_y(0)); } +/// A full context window reads as the trench: row 0 at 100% matches the +/// bottom row at 0%, so the abyss visibly rises as context fills. +#[test] +fn context_fill_drags_the_water_column_toward_the_deep() { + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); + let surface_row = ramp.color_at_context(0, 24, 0); + let abyss_row = ramp.color_at_context(0, 24, 100); + assert_ne!(surface_row, abyss_row); + assert_eq!(abyss_row, ramp.color_at_context(23, 24, 0)); +} + #[test] fn shimmer_is_subtle_and_concentrated_near_the_surface() { - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); - let surface_a = ramp.color_at_phase(0, 20, 0, ShellPhase::Idle); - let surface_b = ramp.color_at_phase(0, 20, 22_500, ShellPhase::Idle); - let deep_a = ramp.color_at_phase(19, 20, 0, ShellPhase::Idle); - let deep_b = ramp.color_at_phase(19, 20, 22_500, ShellPhase::Idle); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); + let surface_a = ramp.color_at_phase_context(0, 20, 0, ShellPhase::Idle, 0); + let surface_b = ramp.color_at_phase_context(0, 20, 22_500, ShellPhase::Idle, 0); + let deep_a = ramp.color_at_phase_context(19, 20, 0, ShellPhase::Idle, 0); + let deep_b = ramp.color_at_phase_context(19, 20, 22_500, ShellPhase::Idle, 0); let surface_shift = distance(surface_a, surface_b); assert!( @@ -356,7 +307,7 @@ fn shimmer_is_subtle_and_concentrated_near_the_surface() { #[test] fn attention_phases_carry_their_own_water_and_work_phases_have_distinct_depth_bias() { - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); // Attention tints are steady — the color itself is the signal, and a // slow breath read as flicker rather than intent — but never neutral: // each attention phase differs from the plain water. @@ -366,14 +317,17 @@ fn attention_phases_carry_their_own_water_and_work_phases_have_distinct_depth_bi ShellPhase::Failed, ] { assert_eq!( - ramp.color_at_phase(4, 20, 0, phase), - ramp.color_at_phase(4, 20, 45_000, phase) + ramp.color_at_phase_context(4, 20, 0, phase, 0), + ramp.color_at_phase_context(4, 20, 45_000, phase, 0) + ); + assert_ne!( + ramp.color_at_phase_context(4, 20, 0, phase, 0), + ramp.color_at_context(4, 20, 0) ); - assert_ne!(ramp.color_at_phase(4, 20, 0, phase), ramp.color_at(4, 20)); } assert_ne!( - ramp.color_at_phase(10, 20, 22_500, ShellPhase::Working), - ramp.color_at_phase(10, 20, 22_500, ShellPhase::Verifying) + ramp.color_at_phase_context(10, 20, 22_500, ShellPhase::Working, 0), + ramp.color_at_phase_context(10, 20, 22_500, ShellPhase::Verifying, 0) ); } @@ -382,27 +336,27 @@ fn tall_columns_darken_continuously_without_an_anchor_shelf() { // The old two-segment ramp met at 0.42 with zero color velocity on both // sides: on a tall window that shelf read as a horizontal seam. The // Bézier column must keep moving through the former anchor zone. - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); let height = 120; let anchor = 50; // ~0.42 of 120 - let above = ramp.color_at(anchor - 6, height); - let at = ramp.color_at(anchor, height); - let below = ramp.color_at(anchor + 6, height); + let above = ramp.color_at_context(anchor - 6, height, 0); + let at = ramp.color_at_context(anchor, height, 0); + let below = ramp.color_at_context(anchor + 6, height, 0); assert_ne!(above, at, "water must still darken entering the old anchor"); assert_ne!(at, below, "water must still darken leaving the old anchor"); - assert_eq!(ramp.color_at(0, height), ramp.surface); - assert_eq!(ramp.color_at(height - 1, height), ramp.deep); + assert_eq!(ramp.color_at_context(0, height, 0), ramp.surface); + assert_eq!(ramp.color_at_context(height - 1, height, 0), ramp.deep); } #[test] fn completion_breath_peaks_once_then_settles() { - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); - let start = ramp.color_at_completion(0, 20, 0); - let peak = ramp.color_at_completion(0, 20, 320); - let settled = ramp.color_at_completion(0, 20, 800); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); + let start = ramp.color_at_completion_context(0, 20, 0, 0); + let peak = ramp.color_at_completion_context(0, 20, 320, 0); + let settled = ramp.color_at_completion_context(0, 20, 800, 0); assert_ne!(start, peak); assert_ne!(peak, settled); - assert_eq!(settled, ramp.color_at(0, 20)); + assert_eq!(settled, ramp.color_at_context(0, 20, 0)); } #[test] @@ -432,6 +386,7 @@ fn cache_fingerprint_changes_when_only_ramp_colors_change() { ShellPhase::Working, true, 1000, + 0, ); let second = OceanColumn::new( second_ramp, @@ -441,6 +396,7 @@ fn cache_fingerprint_changes_when_only_ramp_colors_change() { ShellPhase::Working, true, 1000, + 0, ); assert_ne!(first.color_at_y(viewport.y), second.color_at_y(viewport.y)); @@ -470,6 +426,7 @@ fn each_ramp_color_participates_in_the_typed_cache_identity() { ShellPhase::Working, true, 1000, + 0, ); let alternatives = [ OceanRamp { @@ -507,6 +464,7 @@ fn each_ramp_color_participates_in_the_typed_cache_identity() { ShellPhase::Working, true, 1000, + 0, ); assert_ne!( baseline.ramp_cache_identity(), @@ -518,7 +476,7 @@ fn each_ramp_color_participates_in_the_typed_cache_identity() { #[test] fn identical_semantic_cache_inputs_have_identical_identity() { - let ramp = OceanRamp::for_theme(&crate::palette::UI_THEME).expect("RGB theme"); + let ramp = OceanRamp::for_theme(&crate::palette::UNDERWATER_UI_THEME).expect("underwater ramp"); let viewport = Rect::new(3, 5, 80, 24); let first = OceanColumn::new( ramp, @@ -528,6 +486,7 @@ fn identical_semantic_cache_inputs_have_identical_identity() { ShellPhase::Working, true, 1000, + 0, ); let second = OceanColumn::new( ramp, @@ -537,6 +496,7 @@ fn identical_semantic_cache_inputs_have_identical_identity() { ShellPhase::Working, true, 1000, + 0, ); assert_eq!(first.ramp_cache_identity(), second.ramp_cache_identity()); @@ -545,8 +505,8 @@ fn identical_semantic_cache_inputs_have_identical_identity() { #[test] fn split_shell_surfaces_share_one_absolute_row_column() { - let theme = crate::palette::UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); let viewport = Rect::new(0, 0, 12, 12); let header = Rect::new(0, 0, 12, 2); let composer = Rect::new(0, 10, 12, 2); @@ -563,12 +523,12 @@ fn split_shell_surfaces_share_one_absolute_row_column() { } buf[(4, 10)].set_bg(theme.selection_bg); - let column = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0); + let column = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0, 0); column.paint_matching(header, &mut buf, theme.header_bg); column.paint_matching(composer, &mut buf, theme.composer_bg); - assert_eq!(buf[(0, 0)].bg, ramp.color_at(0, 12)); - assert_eq!(buf[(0, 11)].bg, ramp.color_at(11, 12)); + assert_eq!(buf[(0, 0)].bg, ramp.color_at_context(0, 12, 0)); + assert_eq!(buf[(0, 11)].bg, ramp.color_at_context(11, 12, 0)); assert_ne!(buf[(0, 1)].bg, buf[(0, 10)].bg); assert_eq!( buf[(4, 10)].bg, @@ -579,8 +539,8 @@ fn split_shell_surfaces_share_one_absolute_row_column() { #[test] fn full_viewport_water_column_reaches_both_terminal_edges() { - let theme = crate::palette::UI_THEME; - let ramp = OceanRamp::for_theme(&theme).expect("RGB theme"); + let theme = crate::palette::UNDERWATER_UI_THEME; + let ramp = OceanRamp::for_theme(&theme).expect("underwater ramp"); let viewport = Rect::new(0, 0, 120, 32); let mut buf = Buffer::empty(viewport); for y in viewport.top()..viewport.bottom() { @@ -590,11 +550,11 @@ fn full_viewport_water_column_reaches_both_terminal_edges() { } buf[(60, 16)].set_bg(theme.selection_bg); - let column = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0); + let column = OceanColumn::new(ramp, viewport, 0, None, ShellPhase::Idle, false, 0, 0); column.paint_matching(viewport, &mut buf, theme.surface_bg); for y in viewport.top()..viewport.bottom() { - let expected = ramp.color_at(y, viewport.height); + let expected = ramp.color_at_context(y, viewport.height, 0); assert_eq!(buf[(viewport.left(), y)].bg, expected); assert_eq!(buf[(viewport.right() - 1, y)].bg, expected); } diff --git a/crates/tui/src/tui/theme_picker.rs b/crates/tui/src/tui/theme_picker.rs index 3bc5bc0f3e..8c849141ab 100644 --- a/crates/tui/src/tui/theme_picker.rs +++ b/crates/tui/src/tui/theme_picker.rs @@ -2,16 +2,19 @@ //! //! Built on [`crate::tui::settings_picker`]: navigation, filtering ownership, //! and transactional preview/commit/rollback live in the shared controller. -//! Ocean-specific chrome (swatches, underwater surface, treatment copy) stays -//! here so the framework contract does not flatten visual character. +//! Theme-specific chrome (swatches, underwater surface) stays here so the +//! framework contract does not flatten visual character. //! //! Semantics preserved from the pre-framework picker: //! - Up/Down emit a `ThemeSelectionUpdated{persist:false}` so the host swaps -//! `app.ui_theme` and the ocean treatment immediately and the whole TUI -//! re-paints under the modal. +//! `app.ui_theme` immediately and the whole TUI re-paints under the modal. //! - Enter persists (`persist:true`); Esc emits one more -//! `ThemeSelectionUpdated{persist:false}` to restore the exact theme + -//! treatment pair that was active when the picker opened. +//! `ThemeSelectionUpdated{persist:false}` to restore the exact theme that +//! was active when the picker opened. +//! +//! The option list is 1:1 with [`SELECTABLE_THEMES`] — one row per theme, no +//! modifier rows. `underwater` is an ordinary row: the painted ocean field is +//! the theme, not a treatment beside it. use std::borrow::Cow; use std::cell::RefCell; @@ -28,7 +31,6 @@ use ratatui::{ use crate::localization::{Locale, MessageId, tr}; use crate::palette::{SELECTABLE_THEMES, ThemeId, UiTheme}; use crate::tui::menu_style; -use crate::tui::ocean::OceanTreatment; use crate::tui::settings_picker::{ PickerNavResult, SettingAvailability, SettingOption, SettingValues, SettingsPickerController, SettingsPickerLayout, handle_nav_key, @@ -40,18 +42,17 @@ use crate::tui::views::{ pub struct ThemePickerView { controller: SettingsPickerController, - /// Exact opening state. The controller's option id is only a cursor; the - /// rollback owner needs both persisted fields because Deepsea is a compound - /// theme + treatment choice. + /// Exact opening state for Esc rollback. original_theme_name: String, - original_ocean_treatment: OceanTreatment, + /// Cursor index the controller settled on at open time (row 0 when the + /// persisted selector is not a compiled theme row). Enter without any + /// navigation commits the original name, never this fallback row. + opening_cursor: Option, /// Cached UiTheme for `ThemeId::System`, captured once at construction /// so the per-frame render doesn't re-invoke `UiTheme::detect()` (which /// reads `COLORFGBG`) on every keystroke. system_ui_theme: UiTheme, /// User-configured background applied on top of every named-theme preview. - /// Without carrying this into the picker, a customized Solarized Light - /// session would render Deepsea behind the modal but report Flat inside it. background_override: Option, row_hitboxes: RefCell>, last_mouse_selected: Option, @@ -59,126 +60,26 @@ pub struct ThemePickerView { locale: Locale, } -const DEEPSEA_OPTION_ID: &str = "deepsea"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ThemeSelection { - theme: ThemeId, - ocean_treatment: OceanTreatment, -} - -impl ThemeSelection { - fn for_option_id(option_id: &str) -> Option { - if option_id == DEEPSEA_OPTION_ID { - return Some(Self { - theme: ThemeId::Whale, - ocean_treatment: OceanTreatment::Deepsea, - }); - } - ThemeId::from_name(option_id).map(|theme| Self { - theme, - // Deepsea is explicit consent. Every ordinary theme selection, - // including Match Terminal, Dark, Light, and retained presets, - // returns to the ordinary terminal/theme-owned shell. - ocean_treatment: OceanTreatment::Flat, - }) - } - - const fn treatment_setting(self) -> &'static str { - match self.ocean_treatment { - OceanTreatment::Deepsea => "deepsea", - OceanTreatment::Flat => "flat", - } - } -} - -fn initial_option_id(original_name: &str, original_treatment: OceanTreatment) -> String { - let normalized = original_name.trim().to_ascii_lowercase(); - if original_treatment.is_deepsea() && ThemeId::from_name(&normalized) == Some(ThemeId::Whale) { - DEEPSEA_OPTION_ID.to_string() - } else { - normalized - } -} - -fn theme_options(original_name: &str, original_treatment: OceanTreatment) -> Vec { - let current = original_name.trim().to_ascii_lowercase(); - let current_option = initial_option_id(original_name, original_treatment); - let mut options = Vec::with_capacity(SELECTABLE_THEMES.len() + 1); - for id in SELECTABLE_THEMES.iter().copied() { - let name = id.name(); - options.push( - SettingOption::builder(name, id.display_name()) - .summary(id.tagline()) - .detail(id.tagline()) - .help("Pick a theme with live preview") - .values(SettingValues::new( - Cow::Owned(current.clone()), - // A reset returns to the host-owned terminal surface, - // not a detected palette that can repaint it. - Cow::Borrowed("terminal"), - Cow::Borrowed(name), - )) - .availability(SettingAvailability::Available) - .tab("themes") - .prefer_list_when_narrow(true) - .build(), - ); - if id == ThemeId::WhaleLight { - options.push( - SettingOption::builder(DEEPSEA_OPTION_ID, "Deepsea") - .summary("Ocean field + ambient life, opt-in") - .detail("Paint the authored deep-blue water column behind the Dark palette") - .help("Explicitly opt into the Deepsea surface") - .values(SettingValues::new( - Cow::Owned(current_option.clone()), - Cow::Borrowed("terminal"), - Cow::Borrowed(DEEPSEA_OPTION_ID), - )) - .availability(SettingAvailability::Available) - .tab("themes") - .prefer_list_when_narrow(true) - .build(), - ); - } - } - options -} - impl ThemePickerView { #[cfg(test)] #[must_use] pub fn new(original_name: String) -> Self { - Self::new_with_treatment( - original_name, - crate::tui::ocean::OceanTreatment::Deepsea, - Locale::En, - ) + Self::new_with_background(original_name, Locale::En, None) } - #[cfg(test)] - #[must_use] - pub fn new_with_treatment( - original_name: String, - ocean_treatment: OceanTreatment, - locale: Locale, - ) -> Self { - Self::new_with_treatment_and_background(original_name, ocean_treatment, locale, None) - } - - fn new_with_treatment_and_background( + fn new_with_background( original_name: String, - ocean_treatment: OceanTreatment, locale: Locale, background_override: Option, ) -> Self { - let original_option_id = initial_option_id(&original_name, ocean_treatment); - let options = theme_options(&original_name, ocean_treatment); - let controller = SettingsPickerController::new(options, original_option_id); + let normalized = original_name.trim().to_ascii_lowercase(); + let options = theme_options(&normalized); + let controller = SettingsPickerController::new(options, normalized.clone()); + let opening_cursor = controller.selected_source_index(); Self { controller, - original_theme_name: original_name, - original_ocean_treatment: ocean_treatment, + original_theme_name: normalized, + opening_cursor, system_ui_theme: UiTheme::detect(), background_override, row_hitboxes: RefCell::new(Vec::new()), @@ -191,28 +92,23 @@ impl ThemePickerView { /// Keeping the concrete picker out of that already-large future prevents /// transient modal values from inflating the main-thread stack frame. #[must_use] - pub fn boxed_with_treatment( + pub fn boxed( original_name: String, - ocean_treatment: OceanTreatment, locale: Locale, background_override: Option, ) -> Box { - Box::new(Self::new_with_treatment_and_background( + Box::new(Self::new_with_background( original_name, - ocean_treatment, locale, background_override, )) } - fn current(&self) -> ThemeSelection { + fn current(&self) -> ThemeId { self.controller .selected_id() - .and_then(ThemeSelection::for_option_id) - .unwrap_or(ThemeSelection { - theme: ThemeId::System, - ocean_treatment: OceanTreatment::Flat, - }) + .and_then(ThemeId::from_name) + .unwrap_or(ThemeId::System) } #[cfg(test)] @@ -233,54 +129,32 @@ impl ThemePickerView { } fn preview_event(&self) -> ViewAction { - let selection = self.current(); ViewAction::Emit(ViewEvent::ThemeSelectionUpdated { - theme: selection.theme.name().to_string(), - ocean_treatment: selection.treatment_setting().to_string(), + theme: self.current().name().to_string(), persist: false, }) } fn commit_event(&self) -> ViewAction { - // A commit that never moved the cursor must not rewrite settings. - // - // `ocean_treatment` is independent of `theme` — `normalize_ocean_treatment` - // (settings.rs) accepts `deepsea` beside ANY theme name — but this picker's - // option list can only express `(theme, Flat)` per theme plus the single - // `(Whale, Deepsea)` row. So a persisted pair like `theme = "system"` + - // `ocean_treatment = "deepsea"` opens on the plain `system` row, whose - // option maps to Flat, and pressing Enter without navigating silently - // discarded the user's Deepsea. Treating an unmoved cursor as "nothing - // changed" keeps the unrepresentable pair intact; any real navigation - // still commits the selected option exactly as before. - let opened_on = initial_option_id(&self.original_theme_name, self.original_ocean_treatment); - if self.controller.selected_id() == Some(opened_on.as_str()) { - let ocean_treatment = match self.original_ocean_treatment { - OceanTreatment::Deepsea => "deepsea", - OceanTreatment::Flat => "flat", - }; + // A commit that never moved the cursor must not rewrite settings: + // the persisted theme may be a custom: selector this list + // cannot express as a row, and re-committing the cursor row would + // silently replace it. + if self.controller.selected_source_index() == self.opening_cursor { return ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme: self.original_theme_name.clone(), - ocean_treatment: ocean_treatment.to_string(), persist: true, }); } - let selection = self.current(); ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { - theme: selection.theme.name().to_string(), - ocean_treatment: selection.treatment_setting().to_string(), + theme: self.current().name().to_string(), persist: true, }) } fn revert_event(&self) -> ViewAction { - let ocean_treatment = match self.original_ocean_treatment { - OceanTreatment::Deepsea => "deepsea", - OceanTreatment::Flat => "flat", - }; ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme: self.original_theme_name.clone(), - ocean_treatment: ocean_treatment.to_string(), persist: false, }) } @@ -305,6 +179,32 @@ impl ThemePickerView { } } +fn theme_options(current_name: &str) -> Vec { + let current = current_name.trim().to_ascii_lowercase(); + SELECTABLE_THEMES + .iter() + .copied() + .map(|id| { + let name = id.name(); + SettingOption::builder(name, id.display_name()) + .summary(id.tagline()) + .detail(id.tagline()) + .help("Pick a theme with live preview") + .values(SettingValues::new( + Cow::Owned(current.clone()), + // A reset returns to the host-owned terminal surface, + // not a detected palette that can repaint it. + Cow::Borrowed("terminal"), + Cow::Borrowed(name), + )) + .availability(SettingAvailability::Available) + .tab("themes") + .prefer_list_when_narrow(true) + .build() + }) + .collect() +} + impl ModalView for ThemePickerView { fn kind(&self) -> ModalKind { ModalKind::ThemePicker @@ -364,7 +264,7 @@ impl ModalView for ThemePickerView { // after Enter. We keep the live `surface_bg` (not the shared ink) and // the bare `Clear` so the preview backdrop reads as intended. let current = self.current(); - let live = self.ui_theme_for(current.theme); + let live = self.ui_theme_for(current); let inner = render_underwater_surface(area, buf, tr(self.locale, MessageId::ThemeSurfaceTitle)); @@ -381,20 +281,7 @@ impl ModalView for ThemePickerView { // Theme rows prefer list-when-narrow; layout still drives scroll math. let _layout = SettingsPickerLayout::resolve(content, 34, self.controller.selected_option()); - let mut lines: Vec = Vec::with_capacity(self.controller.visible().len() + 3); - let treatment = if matches!(current.theme, ThemeId::Terminal) { - tr(self.locale, MessageId::ThemeTreatmentDeepseaUnavailable) - } else if current.ocean_treatment.is_flat() - || crate::tui::ocean::OceanRamp::for_theme(&live).is_none() - { - tr(self.locale, MessageId::ThemeTreatmentFlatActive) - } else { - tr(self.locale, MessageId::ThemeTreatmentDeepseaActive) - }; - lines.push(Line::from(Span::styled( - treatment, - Style::default().fg(live.text_hint), - ))); + let mut lines: Vec = Vec::with_capacity(self.controller.visible().len() + 2); lines.push(Line::from("")); let header_rows = lines.len(); @@ -433,11 +320,7 @@ impl ModalView for ThemePickerView { .options() .get(source_idx) .expect("visible source index must reference an option"); - let selection = - ThemeSelection::for_option_id(option.id.as_ref()).unwrap_or(ThemeSelection { - theme: ThemeId::System, - ocean_treatment: OceanTreatment::Flat, - }); + let selection = ThemeId::from_name(option.id.as_ref()).unwrap_or(ThemeId::System); let is_selected = visible_idx == selected_visible; let row_style = if is_selected { menu_style::theme_selected_row_style(&live) @@ -461,36 +344,24 @@ impl ModalView for ThemePickerView { // 3-cell color swatch per row using the candidate theme's own // accent + panel + border colors so the picker doubles as a - // legend. Use the cached resolver so `System` doesn't repeat - // `UiTheme::detect()`. - let row_theme = self.ui_theme_for(selection.theme); - let swatch_colors = if selection.ocean_treatment.is_deepsea() { - crate::tui::ocean::OceanRamp::for_theme(&row_theme).map_or( - [ - row_theme.surface_bg, - row_theme.panel_bg, - row_theme.status_working, - row_theme.mode_yolo, - row_theme.mode_plan, - ], - |ramp| { - [ - ramp.surface, - ramp.middle, - ramp.deep, - ramp.ambient, - row_theme.status_working, - ] - }, - ) - } else { - [ + // legend. The underwater row shows its water column; use the + // cached resolver so `System` doesn't repeat `UiTheme::detect()`. + let row_theme = self.ui_theme_for(selection); + let swatch_colors = match crate::tui::ocean::OceanRamp::for_theme(&row_theme) { + Some(ramp) => [ + ramp.surface, + ramp.middle, + ramp.deep, + ramp.ambient, + row_theme.status_working, + ], + None => [ row_theme.surface_bg, row_theme.panel_bg, row_theme.status_working, row_theme.mode_yolo, row_theme.mode_plan, - ] + ], }; let swatch = swatch_colors .into_iter() @@ -526,49 +397,31 @@ mod tests { KeyEvent::new(code, KeyModifiers::NONE) } - fn selected_values(action: &ViewAction) -> Option<(&str, &str, bool)> { + fn selected_values(action: &ViewAction) -> Option<(&str, bool)> { match action { - ViewAction::Emit(ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - }) - | ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - }) => Some((theme.as_str(), ocean_treatment.as_str(), *persist)), + ViewAction::Emit(ViewEvent::ThemeSelectionUpdated { theme, persist }) + | ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme, persist }) => { + Some((theme.as_str(), *persist)) + } _ => None, } } fn selected_name(action: &ViewAction) -> Option<&str> { - selected_values(action).map(|(theme, _, _)| theme) + selected_values(action).map(|(theme, _)| theme) } #[test] fn opens_at_persisted_theme() { let v = ThemePickerView::new("tokyo-night".to_string()); - assert_eq!( - v.current(), - ThemeSelection { - theme: ThemeId::TokyoNight, - ocean_treatment: OceanTreatment::Flat, - } - ); + assert_eq!(v.current(), ThemeId::TokyoNight); } #[test] fn unknown_persisted_name_falls_back_to_first_row() { let v = ThemePickerView::new("not-a-real-theme".to_string()); assert_eq!(v.selected(), 0); - assert_eq!( - v.current(), - ThemeSelection { - theme: ThemeId::System, - ocean_treatment: OceanTreatment::Flat, - } - ); + assert_eq!(v.current(), ThemeId::System); } #[test] @@ -577,11 +430,11 @@ mod tests { let action = v.handle_key(key(KeyCode::Down)); assert!(matches!(action, ViewAction::Emit(_))); assert_eq!(selected_name(&action), Some(ThemeId::Terminal.name())); - assert_eq!(selected_values(&action), Some(("terminal", "flat", false))); + assert_eq!(selected_values(&action), Some(("terminal", false))); } #[test] - fn mouse_wheel_previews_and_second_deepsea_click_commits_compound_choice() { + fn mouse_wheel_previews_and_second_underwater_click_commits() { let mut v = ThemePickerView::new("system".to_string()); let wheel = v.handle_mouse(MouseEvent { kind: MouseEventKind::ScrollDown, @@ -595,19 +448,19 @@ mod tests { let area = Rect::new(0, 0, 100, 30); let mut buf = Buffer::empty(area); v.render(area, &mut buf); - let deepsea_source = v + let underwater_source = v .controller .options() .iter() - .position(|option| option.id.as_ref() == DEEPSEA_OPTION_ID) - .expect("Deepsea row"); + .position(|option| option.id.as_ref() == ThemeId::Underwater.name()) + .expect("Underwater row"); let (rect, idx) = v .row_hitboxes .borrow() .iter() .copied() - .find(|(_, source)| *source == deepsea_source) - .expect("rendered Deepsea hitbox"); + .find(|(_, source)| *source == underwater_source) + .expect("rendered Underwater hitbox"); let click = MouseEvent { kind: MouseEventKind::Down(MouseButton::Left), column: rect.x, @@ -617,10 +470,10 @@ mod tests { let preview = v.handle_mouse(click); assert!(matches!(preview, ViewAction::Emit(_))); assert_eq!(v.selected(), idx); - assert_eq!(selected_values(&preview), Some(("dark", "deepsea", false))); + assert_eq!(selected_values(&preview), Some(("underwater", false))); let commit = v.handle_mouse(click); assert!(matches!(commit, ViewAction::EmitAndClose(_))); - assert_eq!(selected_values(&commit), Some(("dark", "deepsea", true))); + assert_eq!(selected_values(&commit), Some(("underwater", true))); } #[test] @@ -641,13 +494,8 @@ mod tests { v.handle_key(key(KeyCode::Char('7'))); // -> CatppuccinMocha let action = v.handle_key(key(KeyCode::Enter)); match action { - ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - }) => { + ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme, persist }) => { assert_eq!(theme, ThemeId::CatppuccinMocha.name()); - assert_eq!(ocean_treatment, "flat"); assert!(persist); } other => panic!("expected commit, got {other:?}"), @@ -655,66 +503,41 @@ mod tests { } #[test] - fn enter_without_navigating_preserves_a_non_whale_deepsea_pair() { - // `ocean_treatment` is independent of `theme` (settings.rs - // normalize_ocean_treatment accepts deepsea beside any theme), and - // esc_reverts_to_exact_original_theme_and_treatment_pair already pins - // that Esc keeps such a pair. Enter used to destroy it: the picker can - // only express Deepsea paired with Whale, so (dracula, deepsea) opened - // on the plain `dracula` row and committing without navigating wrote - // flat, silently discarding a persisted preference. #5698 review - // finding 3. - let mut v = ThemePickerView::new_with_treatment( - "dracula".to_string(), - OceanTreatment::Deepsea, - Locale::En, - ); + fn enter_without_navigating_preserves_a_custom_theme_selector() { + // The picker's rows are compiled themes only; a persisted + // custom: selector opens on no row, and Enter without + // navigation must not replace it with a compiled row. + let mut v = ThemePickerView::new("custom:midnight".to_string()); let action = v.handle_key(key(KeyCode::Enter)); assert_eq!( selected_values(&action), - Some(("dracula", "deepsea", true)), - "committing without moving the cursor must not downgrade the treatment" + Some(("custom:midnight", true)), + "committing without moving the cursor must not replace the persisted selector" ); } #[test] fn enter_after_navigating_away_still_commits_the_chosen_option() { - // The preservation above must not freeze the picker: a real move still - // commits the selected option's own pair. - let mut v = ThemePickerView::new_with_treatment( - "dracula".to_string(), - OceanTreatment::Deepsea, - Locale::En, - ); + let mut v = ThemePickerView::new("dracula".to_string()); v.handle_key(key(KeyCode::Down)); let action = v.handle_key(key(KeyCode::Enter)); - let (theme, treatment, persist) = selected_values(&action).expect("expected a commit"); + let (theme, persist) = selected_values(&action).expect("expected a commit"); assert_ne!( theme, "dracula", "navigation should have moved off the opening row" ); - assert_eq!(treatment, "flat"); assert!(persist); } #[test] - fn esc_reverts_to_exact_original_theme_and_treatment_pair() { - let mut v = ThemePickerView::new_with_treatment( - "dracula".to_string(), - OceanTreatment::Deepsea, - Locale::En, - ); + fn esc_reverts_to_exact_original_theme() { + let mut v = ThemePickerView::new("dracula".to_string()); v.handle_key(key(KeyCode::Up)); v.handle_key(key(KeyCode::Up)); let action = v.handle_key(key(KeyCode::Esc)); match action { - ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - }) => { + ViewAction::EmitAndClose(ViewEvent::ThemeSelectionUpdated { theme, persist }) => { assert_eq!(theme, "dracula"); - assert_eq!(ocean_treatment, "deepsea"); assert!(!persist); } other => panic!("expected revert, got {other:?}"), @@ -722,11 +545,11 @@ mod tests { } #[test] - fn digit_jumps_to_deepsea_and_previews_compound_choice() { + fn digit_jumps_to_underwater_and_previews() { let mut v = ThemePickerView::new("system".to_string()); - let action = v.handle_key(key(KeyCode::Char('5'))); - // Deepsea follows System, Terminal, Dark, and Light. - assert_eq!(selected_values(&action), Some(("dark", "deepsea", false))); + let action = v.handle_key(key(KeyCode::Char('3'))); + // Underwater follows System and Terminal. + assert_eq!(selected_values(&action), Some(("underwater", false))); } #[test] @@ -759,71 +582,6 @@ mod tests { v.render(area, &mut buf); } - #[test] - fn treatment_report_names_effective_appearance() { - let area = ratatui::layout::Rect::new(0, 0, 100, 30); - - let flat = ThemePickerView::new_with_treatment( - "dark".to_string(), - crate::tui::ocean::OceanTreatment::Flat, - Locale::En, - ); - let mut flat_buf = ratatui::buffer::Buffer::empty(area); - flat.render(area, &mut flat_buf); - let flat_text = flat_buf - .content() - .iter() - .map(|cell| cell.symbol()) - .collect::(); - assert!(flat_text.contains("Treatment Flat — active")); - - let terminal = ThemePickerView::new_with_treatment( - "terminal".to_string(), - crate::tui::ocean::OceanTreatment::Deepsea, - Locale::En, - ); - let mut terminal_buf = ratatui::buffer::Buffer::empty(area); - terminal.render(area, &mut terminal_buf); - let terminal_text = terminal_buf - .content() - .iter() - .map(|cell| cell.symbol()) - .collect::(); - assert!(terminal_text.contains("Deepsea unavailable")); - assert!(terminal_text.contains("Terminal owns the background")); - - let solarized = ThemePickerView::new_with_treatment( - "solarized-light".to_string(), - crate::tui::ocean::OceanTreatment::Deepsea, - Locale::En, - ); - let mut solarized_buf = ratatui::buffer::Buffer::empty(area); - solarized.render(area, &mut solarized_buf); - let solarized_text = solarized_buf - .content() - .iter() - .map(|cell| cell.symbol()) - .collect::(); - assert!(solarized_text.contains("Treatment Flat — active")); - assert!(!solarized_text.contains("Treatment Deepsea — active")); - - let deepsea = ThemePickerView::new_with_treatment( - "dark".to_string(), - OceanTreatment::Deepsea, - Locale::En, - ); - let mut deepsea_buf = ratatui::buffer::Buffer::empty(area); - deepsea.render(area, &mut deepsea_buf); - let deepsea_text = deepsea_buf - .content() - .iter() - .map(|cell| cell.symbol()) - .collect::(); - assert!(deepsea_text.contains("Treatment Deepsea — active")); - assert!(!deepsea_text.contains("Treatment Flat — active")); - assert!(deepsea_text.contains("Deepsea")); - } - #[test] fn every_selectable_theme_previews_and_renders_through_the_same_surface() { let area = ratatui::layout::Rect::new(0, 0, 100, 32); @@ -837,16 +595,10 @@ mod tests { .position(|option| option.id.as_ref() == expected.name()) .expect("selectable theme option"); let _ = view.controller.select_source_index(index); - assert_eq!( - view.current(), - ThemeSelection { - theme: expected, - ocean_treatment: OceanTreatment::Flat, - } - ); + assert_eq!(view.current(), expected); assert_eq!( selected_values(&view.preview_event()), - Some((expected.name(), "flat", false)) + Some((expected.name(), false)) ); let mut buf = ratatui::buffer::Buffer::empty(area); @@ -861,7 +613,6 @@ mod tests { "{} was not represented in its live preview surface", expected.name() ); - assert!(text.contains("Treatment")); assert!(text.contains("Enter save")); } } @@ -950,21 +701,22 @@ mod tests { let v = ThemePickerView::new("dracula".to_string()); assert_eq!(v.controller.original_id(), "dracula"); assert_eq!(v.controller.selected_id(), Some("dracula")); - assert_eq!(v.controller.visible().len(), SELECTABLE_THEMES.len() + 1); + // One row per selectable theme: no modifier rows beside them. + assert_eq!(v.controller.visible().len(), SELECTABLE_THEMES.len()); } } use unicode_width::UnicodeWidthStr as _TidelineWidth; // --------------------------------------------------------------------------- -// Tideline theme list (spec §5a "Theme list"): the 13 selectable themes +// Tideline theme list (spec §5a "Theme list"): the 14 selectable themes // (4 mode rows + 9 presets), the selected row boxed with ✓, and the MOTION // (OPTIONAL) toggles. Translation scaffolding in the topbar mold: pure, // deterministic, injected selection — Up/Down preview and Enter apply stay // the shared settings-picker controller's job at the landing slice; not // wired into `ui/frame.rs` (#5698 gate). -/// The 13 themes in display order: 4 mode rows then 9 presets. +/// The 14 themes in display order: 4 mode rows then 10 presets. #[allow(dead_code)] // translation scaffolding: wired by the landing slice pub fn tideline_theme_rows() -> Vec { crate::palette::SELECTABLE_THEMES.to_vec() @@ -974,7 +726,7 @@ pub fn tideline_theme_rows() -> Vec { #[allow(dead_code)] // translation scaffolding: wired by the landing slice pub struct TidelineThemeList<'a> { pub theme: &'a UiTheme, - /// Selected row index into the 13-theme display order. + /// Selected row index into the 14-theme display order. pub selected: usize, /// `low_motion` setting (MOTION OPTIONAL toggle 1). pub low_motion: bool, @@ -1036,7 +788,7 @@ fn tchrome(theme: &UiTheme, ink: crate::palette::ChromeInk) -> Style { crate::palette::chrome_style(theme, ink) } -/// Paint the theme list: 13 rows (4 modes + 9 presets) with the selected +/// Paint the theme list: 14 rows (4 modes + 10 presets) with the selected /// row boxed `[ ✓ Name ]`, then the MOTION (OPTIONAL) toggle rows. #[allow(dead_code)] // translation scaffolding: wired by the landing slice pub fn render_tideline_theme_list(area: Rect, buf: &mut Buffer, list: &TidelineThemeList<'_>) { diff --git a/crates/tui/src/tui/theme_picker/tideline_tests.rs b/crates/tui/src/tui/theme_picker/tideline_tests.rs index 0a48cb1a1b..ef4caba5e7 100644 --- a/crates/tui/src/tui/theme_picker/tideline_tests.rs +++ b/crates/tui/src/tui/theme_picker/tideline_tests.rs @@ -7,8 +7,8 @@ use crate::palette::SELECTABLE_THEMES; use crate::tui::golden_harness::render_golden_text; #[test] -fn theme_rows_are_the_thirteen_selectable_themes() { - assert_eq!(tideline_theme_rows().len(), 13); +fn theme_rows_are_the_fourteen_selectable_themes() { + assert_eq!(tideline_theme_rows().len(), 14); assert_eq!(tideline_theme_rows().as_slice(), SELECTABLE_THEMES); } diff --git a/crates/tui/src/tui/ui/apply.rs b/crates/tui/src/tui/ui/apply.rs index 343153b78f..eccd9d35be 100644 --- a/crates/tui/src/tui/ui/apply.rs +++ b/crates/tui/src/tui/ui/apply.rs @@ -2019,14 +2019,12 @@ pub(crate) async fn apply_command_result( // Avoids re-reading settings.toml from disk on every // `/theme` invocation. let original = app.theme_id.name().to_string(); - app.view_stack.push_boxed( - crate::tui::theme_picker::ThemePickerView::boxed_with_treatment( + app.view_stack + .push_boxed(crate::tui::theme_picker::ThemePickerView::boxed( original, - app.ocean_treatment, app.ui_locale, app.background_color_override, - ), - ); + )); } } AppAction::OpenSkillsManager => { diff --git a/crates/tui/src/tui/ui/event_loop.rs b/crates/tui/src/tui/ui/event_loop.rs index e843f765f3..8966ce8411 100644 --- a/crates/tui/src/tui/ui/event_loop.rs +++ b/crates/tui/src/tui/ui/event_loop.rs @@ -3762,10 +3762,10 @@ pub(crate) async fn run_event_loop( let active_cell_has_live_motion = active_cell_has_live_motion(app); let translation_placeholder_has_live_motion = app.translation_enabled && (pending_thinking_translations > 0 || app.streaming_thinking_active_entry.is_some()); - // The ordinary terminal stays quiet. Only the explicit underwater - // treatment earns ambient redraws; its column can breathe at any - // usable size and its life needs the collision-safe water budget. - let underwater_atmosphere_enabled = app.ocean_treatment.is_deepsea(); + // The ordinary terminal stays quiet. Only the underwater theme earns + // ambient redraws; its column can breathe at any usable size and its + // life needs the collision-safe water budget. + let underwater_atmosphere_enabled = app.theme_id == crate::palette::ThemeId::Underwater; let deepsea_field_breathes = underwater_atmosphere_enabled && crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme).is_some(); let browsing_history = !app.viewport.transcript_scroll.is_at_tail(); @@ -4269,6 +4269,9 @@ pub(crate) async fn run_event_loop( { return Ok(()); } + if app.pending_launch_action.is_none() { + restore_launch_card_after_view_close(app); + } if let Some(action) = app.pending_launch_action.take() { match action { crate::tui::underwater::LaunchAction::None => {} @@ -4276,8 +4279,8 @@ pub(crate) async fn run_event_loop( app.launch.status = Some(app.tr(MessageId::LaunchCreatingWorktree).into_owned()); match provision_launch_worktree(app.workspace.clone(), name).await { - Ok(workspace) => { - let result = begin_launch_session(app, Some(workspace)); + Ok(provisioned) => { + let result = begin_launch_worktree_session(app, provisioned); if apply_command_result( terminal, app, @@ -4301,12 +4304,14 @@ pub(crate) async fn run_event_loop( } } crate::tui::underwater::LaunchAction::Resume => { - // A launched command dissolves the card. - app.launch.dissolve_card(app.ambient_clock_ms); if app.launch.workspace_session_count == 0 { + // Nothing to open: the card stays and says so. app.launch.status = Some(app.tr(MessageId::LaunchNoSavedSessions).into_owned()); } else { + // A launched command dissolves the card; Esc + // out of the picker brings it back. + app.launch.dissolve_card(app.ambient_clock_ms); app.view_stack .push(SessionPickerView::new(&app.workspace, app.ui_locale)); } @@ -4744,6 +4749,7 @@ pub(crate) async fn run_event_loop( { return Ok(()); } + restore_launch_card_after_view_close(app); continue; } @@ -4782,9 +4788,15 @@ pub(crate) async fn run_event_loop( crate::tui::underwater::LaunchComposerKey::MenuNavigate(delta) => { // The card is up: Up/Down move its menu selection. let entries = crate::tui::underwater::LAUNCH_MENU_ENTRIES as i32; - app.launch.menu_selected = (app.launch.menu_selected as i32 + delta) - .rem_euclid(entries) - as usize; + // First arrow lands on the first (Up: last) + // entry; from there it moves. + app.launch.menu_selected = Some(match app.launch.menu_selected { + None if delta < 0 => (entries - 1) as usize, + None => 0, + Some(current) => { + (current as i32 + delta).rem_euclid(entries) as usize + } + }); app.needs_redraw = true; continue; } @@ -4858,8 +4870,8 @@ pub(crate) async fn run_event_loop( app.launch.status = Some(app.tr(MessageId::LaunchCreatingWorktree).into_owned()); match provision_launch_worktree(app.workspace.clone(), name).await { - Ok(workspace) => { - let result = begin_launch_session(app, Some(workspace)); + Ok(provisioned) => { + let result = begin_launch_worktree_session(app, provisioned); if apply_command_result( terminal, app, @@ -4883,12 +4895,14 @@ pub(crate) async fn run_event_loop( } } crate::tui::underwater::LaunchAction::Resume => { - // A launched command dissolves the card. - app.launch.dissolve_card(app.ambient_clock_ms); if app.launch.workspace_session_count == 0 { + // Nothing to open: the card stays and says so. app.launch.status = Some(app.tr(MessageId::LaunchNoSavedSessions).into_owned()); } else { + // A launched command dissolves the card; Esc + // out of the picker brings it back. + app.launch.dissolve_card(app.ambient_clock_ms); app.view_stack .push(SessionPickerView::new(&app.workspace, app.ui_locale)); } diff --git a/crates/tui/src/tui/ui/frame.rs b/crates/tui/src/tui/ui/frame.rs index 98c7a15f4b..fc56a74d87 100644 --- a/crates/tui/src/tui/ui/frame.rs +++ b/crates/tui/src/tui/ui/frame.rs @@ -93,20 +93,23 @@ pub(crate) fn info_segments(app: &App, width: u16) -> Vec { )); } - // The context reading: painted here and nowhere else. At the 80% cap - // the whole reading turns to the error token — it is the one fact on + // The context reading: painted here and nowhere else. Only displayed + // when context fullness >= 50%; below 50% it remains silent. At the 80% + // cap the whole reading turns to the error token — it is the one fact on // this row that becomes a problem rather than a status. let pct = info_context_percent(app); - segments.push(InfoSegment::new( - InfoSegmentId::Context, - app.tr(MessageId::InfoLineContext).as_ref(), - format!("{pct}%"), - if pct >= 80 { - ChromeInk::Failure - } else { - ChromeInk::Info - }, - )); + if pct >= 50 { + segments.push(InfoSegment::new( + InfoSegmentId::Context, + app.tr(MessageId::InfoLineContext).as_ref(), + format!("{pct}%"), + if pct >= 80 { + ChromeInk::Failure + } else { + ChromeInk::Info + }, + )); + } let cost = session_cost_label(app); if !cost.is_empty() { @@ -142,12 +145,43 @@ pub(crate) fn info_segments(app: &App, width: u16) -> Vec { ChromeInk::MetadataValue, )); } + let hit = u64::from(app.session.displayed_total_cache_hit_tokens()); + let miss = u64::from(app.session.displayed_total_cache_miss_tokens()); + let cache_total = hit + miss; + if cache_total > 0 { + let cache_pct = (hit * 100 + cache_total / 2) + .checked_div(cache_total) + .and_then(|pct| u8::try_from(pct).ok()) + .unwrap_or(100); + segments.push(InfoSegment::new( + InfoSegmentId::Cache, + "cache", + format!("{cache_pct}%"), + ChromeInk::MetadataValue, + )); + } segments.push(InfoSegment::new( InfoSegmentId::OutputTokens, "↓", crate::tui::session_metrics::format_tokens(tokens), ChromeInk::MetadataValue, )); + } else { + let hit = u64::from(app.session.displayed_total_cache_hit_tokens()); + let miss = u64::from(app.session.displayed_total_cache_miss_tokens()); + let cache_total = hit + miss; + if cache_total > 0 { + let cache_pct = (hit * 100 + cache_total / 2) + .checked_div(cache_total) + .and_then(|pct| u8::try_from(pct).ok()) + .unwrap_or(100); + segments.push(InfoSegment::new( + InfoSegmentId::Cache, + "cache", + format!("{cache_pct}%"), + ChromeInk::MetadataValue, + )); + } } segments diff --git a/crates/tui/src/tui/ui/frame/one_owner_tests.rs b/crates/tui/src/tui/ui/frame/one_owner_tests.rs index 6a15983729..bc3f96d1f6 100644 --- a/crates/tui/src/tui/ui/frame/one_owner_tests.rs +++ b/crates/tui/src/tui/ui/frame/one_owner_tests.rs @@ -115,9 +115,9 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() { let (mode, permission) = crate::tui::underwater::posture_chips(&app); let mode = mode.expect("mode chip").0.into_owned(); let permission = permission.expect("permission chip").0.into_owned(); - let facts = [ - ("context reading", format!("ctx {pct}%")), - ("context percent", format!("{pct}%")), + // The context reading stays silent below 50% fullness and paints + // exactly once at or above it. + let mut facts = vec![ ("mode chip", format!("· {mode} (")), ("permission chip", format!("▶▶ {permission} (")), ("model", model), @@ -130,6 +130,17 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() { ("output rate", "40 tok/s".to_string()), ("ttft", "ttft 400ms".to_string()), ]; + if pct >= 50 { + facts.push(("context reading", format!("ctx {pct}%"))); + facts.push(("context percent", format!("{pct}%"))); + } else { + assert_eq!( + count_rows_containing(&rows, "ctx "), + 0, + "{width}x{height}: ctx stays silent below 50%:\n{}", + rows.join("\n") + ); + } for (name, needle) in facts { if needle.is_empty() { continue; @@ -149,7 +160,7 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() { .expect("posture bar"); let metrics = rows .iter() - .position(|row| row.contains(&format!("ctx {pct}%"))) + .position(|row| row.contains("tok/s")) .expect("metrics line"); let composer = app .viewport @@ -186,8 +197,9 @@ fn idle_frame_keeps_two_chrome_rows_and_last_turn_metrics() { let rows = draw(&mut app, 100, 32); let composer = app.viewport.last_composer_area.unwrap().bottom() as usize; assert!(rows[composer].starts_with("▶▶"), "{}", rows[composer]); + // The idle fixture sits at 0% context, so the reading stays silent. assert!( - rows[composer + 1].contains("ctx "), + !rows[composer + 1].contains("ctx "), "{}", rows[composer + 1] ); diff --git a/crates/tui/src/tui/ui/handlers.rs b/crates/tui/src/tui/ui/handlers.rs index 7a2ccadd76..942a8cf800 100644 --- a/crates/tui/src/tui/ui/handlers.rs +++ b/crates/tui/src/tui/ui/handlers.rs @@ -958,15 +958,14 @@ async fn handle_theme_selection_updated( engine_handle: &mut EngineHandle, web_config_session: &mut Option, theme: String, - ocean_treatment: String, persist: bool, ) -> Result { let result = prepare_config_update_result( - commands::set_theme_selection(app, &theme, &ocean_treatment, persist), + commands::set_config_value(app, "theme", &theme, persist), persist, ); - // Both halves affect shell paint and must bypass ratatui's incremental - // cell diff, including a Deepsea -> Flat preview or Esc rollback. + // The theme owns the shell paint and must bypass ratatui's incremental + // cell diff, including an Esc rollback. app.force_next_full_repaint = true; if apply_command_result( terminal, @@ -1301,11 +1300,7 @@ pub(crate) async fn handle_view_events( return Ok(true); } } - ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - } => { + ViewEvent::ThemeSelectionUpdated { theme, persist } => { if handle_theme_selection_updated( terminal, app, @@ -1314,7 +1309,6 @@ pub(crate) async fn handle_view_events( engine_handle, web_config_session, theme, - ocean_treatment, persist, ) .await? @@ -2287,11 +2281,7 @@ pub(crate) fn handle_view_events_boxed<'a>( return Ok(true); } } - ViewEvent::ThemeSelectionUpdated { - theme, - ocean_treatment, - persist, - } => { + ViewEvent::ThemeSelectionUpdated { theme, persist } => { if handle_theme_selection_updated( terminal, app, @@ -2300,7 +2290,6 @@ pub(crate) fn handle_view_events_boxed<'a>( engine_handle, web_config_session, theme, - ocean_treatment, persist, ) .await? diff --git a/crates/tui/src/tui/ui/overlays.rs b/crates/tui/src/tui/ui/overlays.rs index fce363090a..39af396ea4 100644 --- a/crates/tui/src/tui/ui/overlays.rs +++ b/crates/tui/src/tui/ui/overlays.rs @@ -58,14 +58,12 @@ pub(crate) fn open_theme_picker(app: &mut App) { return; } let original = app.theme_id.name().to_string(); - app.view_stack.push_boxed( - crate::tui::theme_picker::ThemePickerView::boxed_with_treatment( + app.view_stack + .push_boxed(crate::tui::theme_picker::ThemePickerView::boxed( original, - app.ocean_treatment, app.ui_locale, app.background_color_override, - ), - ); + )); app.needs_redraw = true; } @@ -84,6 +82,22 @@ pub(crate) fn toggle_help_view(app: &mut App) { app.needs_redraw = true; } +/// After a shared view closes over the launch screen, bring the launch card +/// back: Esc out of the resume picker or the changelog pager returns to the +/// card rather than stranding the user on an empty stage. A view that began +/// a session (`launch.visible == false`) or a draft in the composer leaves +/// the dissolved card alone. +pub(crate) fn restore_launch_card_after_view_close(app: &mut App) { + if app.launch.visible + && app.view_stack.is_empty() + && app.launch.dissolve_started_ms.is_some() + && app.input.is_empty() + { + app.launch.restore_card(); + app.needs_redraw = true; + } +} + /// Choose which durable-task summaries should appear in the Work /// sidebar's Tasks panel. /// diff --git a/crates/tui/src/tui/ui/session_state.rs b/crates/tui/src/tui/ui/session_state.rs index 6a73a7b5d7..194464a8d6 100644 --- a/crates/tui/src/tui/ui/session_state.rs +++ b/crates/tui/src/tui/ui/session_state.rs @@ -609,13 +609,30 @@ pub(crate) fn launch_worktree_spec( pub(crate) async fn provision_launch_worktree( workspace: PathBuf, requested: String, -) -> Result { +) -> Result { let spec = launch_worktree_spec(&workspace, &requested)?; - let provisioned = - tokio::task::spawn_blocking(move || codewhale_lane::provision_worktree(&spec)) - .await - .context("new worktree task failed")??; - Ok(provisioned.path) + tokio::task::spawn_blocking(move || codewhale_lane::provision_worktree(&spec)) + .await + .context("new worktree task failed")? +} + +/// Start the launch session inside a freshly provisioned worktree and leave a +/// receipt in the transcript saying where it went: the card's New worktree +/// entry used to succeed silently, which reads as having done nothing. +pub(crate) fn begin_launch_worktree_session( + app: &mut App, + provisioned: codewhale_lane::ProvisionedWorktree, +) -> commands::CommandResult { + let receipt = app + .tr(MessageId::LaunchWorktreeCreated) + .replace("{path}", &provisioned.path.display().to_string()) + .replace("{branch}", &provisioned.branch); + let result = begin_launch_session(app, Some(provisioned.path)); + app.add_message(HistoryCell::System { + content: receipt.clone(), + }); + app.status_message = Some(receipt); + result } pub(crate) fn begin_launch_session( @@ -1194,3 +1211,116 @@ mod stall_outbox_tests { ); } } + +#[cfg(test)] +mod launch_worktree_tests { + use super::*; + + fn git(dir: &std::path::Path, args: &[&str]) { + let status = std::process::Command::new("git") + .current_dir(dir) + .args(args) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .expect("git runs"); + assert!(status.success(), "git {args:?} in {}", dir.display()); + } + + /// The launch card's New worktree entry must produce a real, checked-out + /// worktree and hand the new session that path — not just print a status. + #[tokio::test] + async fn new_worktree_creates_a_checkout_and_the_session_starts_inside_it() { + let root = tempfile::tempdir().expect("tempdir"); + let repo = root.path().join("proj"); + std::fs::create_dir_all(&repo).unwrap(); + git(&repo, &["init", "-q", "-b", "main"]); + // Windows CI checks out with a global core.autocrlf=true; the + // byte-fidelity assertion below needs the worktree checkout to be + // verbatim. + git(&repo, &["config", "core.autocrlf", "false"]); + git( + &repo, + &[ + "-c", + "user.email=t@t", + "-c", + "user.name=t", + "commit", + "-q", + "--allow-empty", + "-m", + "root", + ], + ); + std::fs::write(repo.join("README.md"), "hello\n").unwrap(); + git(&repo, &["add", "README.md"]); + git( + &repo, + &[ + "-c", + "user.email=t@t", + "-c", + "user.name=t", + "commit", + "-q", + "-m", + "readme", + ], + ); + + let provisioned = provision_launch_worktree(repo.clone(), "Fix Login / v2".to_string()) + .await + .expect("worktree provisioned"); + let path = provisioned.path.clone(); + assert_eq!(provisioned.branch, "codex/fix-login-v2"); + // git reports the canonical toplevel (macOS: /private/var…), so + // compare canonical forms. + assert_eq!( + path.canonicalize().unwrap(), + root.path() + .join(".codewhale-worktrees") + .join("proj-fix-login-v2") + .canonicalize() + .unwrap() + ); + assert!(path.join(".git").exists(), "worktree is a git checkout"); + assert_eq!( + std::fs::read_to_string(path.join("README.md")).unwrap(), + "hello\n", + "worktree carries HEAD's files" + ); + let head = std::process::Command::new("git") + .current_dir(&path) + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .output() + .unwrap(); + assert_eq!( + String::from_utf8_lossy(&head.stdout).trim(), + "codex/fix-login-v2" + ); + + // A second request for the same name says so instead of clobbering. + let err = provision_launch_worktree(repo.clone(), "fix login v2".to_string()) + .await + .expect_err("duplicate path refused"); + assert!(err.to_string().contains("already exists"), "{err}"); + + // The launch session is pointed at the worktree, not the origin repo. + let dir = tempfile::tempdir().unwrap(); + let mut app = App::new( + crate::test_support::test_tui_options(dir.path()), + &Config::default(), + ); + app.launch.visible = true; + let result = begin_launch_worktree_session(&mut app, provisioned); + assert_eq!(app.workspace, path); + assert!(!app.launch.visible); + let receipt = app.status_message.clone().expect("receipt"); + assert!(receipt.contains("codex/fix-login-v2") && receipt.contains("proj-fix-login-v2")); + assert!(matches!( + result.action, + Some(AppAction::SyncSession { workspace, .. }) if workspace == path + )); + } +} diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 23da8e8cd3..eb400ba5b4 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -4497,8 +4497,8 @@ fn bottom_placement_keeps_the_stage_and_queued_preview_at_twelve_rows() { #[test] fn wide_underwater_canvas_carries_the_ocean_to_both_terminal_edges() { let mut app = create_test_app(); - app.ui_theme = crate::palette::UI_THEME; - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = crate::palette::UNDERWATER_UI_THEME; app.onboarding_workspace_trust_gate = false; app.onboarding = OnboardingState::None; let surface_bg = app.ui_theme.surface_bg; @@ -5678,13 +5678,13 @@ fn session_denied_notice_explains_cached_decision_and_recovery() { async fn cached_denial_explanation_survives_tool_completion_and_done_render() { use crate::core::engine::MockApprovalEvent; use crate::tools::spec::ToolError; - use crate::tui::ocean::OceanTreatment; use ratatui::{Terminal, backend::TestBackend}; let mut app = create_test_app(); app.onboarding = OnboardingState::None; app.launch.visible = false; - app.ocean_treatment = OceanTreatment::Deepsea; + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = crate::palette::UNDERWATER_UI_THEME; app.is_loading = true; app.runtime_turn_status = Some("in_progress".to_string()); diff --git a/crates/tui/src/tui/underwater.rs b/crates/tui/src/tui/underwater.rs index 0d94e241db..528a5e008e 100644 --- a/crates/tui/src/tui/underwater.rs +++ b/crates/tui/src/tui/underwater.rs @@ -134,7 +134,10 @@ pub fn run_launch_menu_entry( launch: &mut crate::tui::app::LaunchState, locale: Locale, ) -> LaunchAction { - match launch.menu_selected % LAUNCH_MENU_ENTRIES { + let Some(selected) = launch.menu_selected else { + return LaunchAction::None; + }; + match selected % LAUNCH_MENU_ENTRIES { 0 => { open_launch_worktree_prompt(launch, locale); LaunchAction::None @@ -157,8 +160,10 @@ pub fn run_launch_menu_entry( /// empty Enter, the launch chords, and submitting. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LaunchComposerKey { - /// The key is fully consumed and does nothing (Enter on an empty - /// composer: there is no row to run and nothing to send). + /// The key is fully consumed and does nothing more (Enter on an empty + /// composer with no menu entry highlighted: there is no row to run and + /// nothing to send; Esc clearing the menu highlight or bringing the + /// card back). Consumed, /// Submit the composed message through the normal dispatch path. Submit, @@ -176,8 +181,8 @@ pub enum LaunchComposerKey { ComposerAuthority, /// Move the launch card's menu selection (Up/Down while the card is up). MenuNavigate(i32), - /// Run the card's highlighted menu entry (Enter while the card is up and - /// the composer is empty). + /// Run the card's highlighted menu entry (Enter while the card is up, + /// the composer is empty, and the user has arrowed onto an entry). MenuRun, } @@ -212,9 +217,9 @@ pub fn handle_launch_composer_key(app: &mut App, key: KeyEvent) -> LaunchCompose app.close_slash_menu(); } if app.input.trim().is_empty() { - if card_up { - // The card owns Enter while it is up: run the - // highlighted menu entry. + if card_up && app.launch.menu_selected.is_some() { + // The card owns Enter only once the user has arrowed + // onto an entry; an untouched menu runs nothing. return LaunchComposerKey::MenuRun; } LaunchComposerKey::Consumed @@ -225,6 +230,17 @@ pub fn handle_launch_composer_key(app: &mut App, key: KeyEvent) -> LaunchCompose } KeyCode::Up if card_up => LaunchComposerKey::MenuNavigate(-1), KeyCode::Down if card_up => LaunchComposerKey::MenuNavigate(1), + // Esc walks back one step: a highlighted menu entry is unhighlighted; + // an empty composer with the card gone brings the card back. A draft + // in the composer keeps Esc's composer meaning. + KeyCode::Esc if card_up && app.launch.menu_selected.is_some() => { + app.launch.menu_selected = None; + LaunchComposerKey::Consumed + } + KeyCode::Esc if !card_up && app.input.is_empty() => { + app.launch.restore_card(); + LaunchComposerKey::Consumed + } KeyCode::Char('r' | 'n' | 'l' | 'q') if key.modifiers.contains(KeyModifiers::CONTROL) => { LaunchComposerKey::MenuChord } @@ -1887,7 +1903,7 @@ pub fn empty_state_lines(app: &App, area: Rect) -> Vec> { app.mcp_configured_count, width, ); - let brand = "Codewhale"; + let brand = "codewhale"; let brand_inset = " ".repeat(width.saturating_sub(brand.width()) / 2); lines.push(Line::from(Span::styled( format!("{brand_inset}{brand}"), @@ -1931,7 +1947,7 @@ mod launch_contract_tests { composer_focus: true, composer_area: None, send_area: None, - menu_selected: 0, + menu_selected: None, dissolve_started_ms: None, claude_code_detected: false, } @@ -2028,7 +2044,8 @@ mod launch_composer_tests { use super::{ LaunchAction, LaunchComposerKey, apply_launch_hitboxes, handle_launch_composer_key, handle_launch_key, launch_composer_rows, render_launch_completion_popup, - render_tideline_startup, tideline_startup_from_app, tideline_startup_hitboxes, + render_tideline_startup, run_launch_menu_entry, tideline_startup_from_app, + tideline_startup_hitboxes, }; use crate::localization::{Locale, MessageId, tr}; use crate::tui::app::App; @@ -2390,28 +2407,81 @@ mod launch_composer_tests { assert_eq!(app.handle_composer_enter().as_deref(), Some("hello world")); assert!(app.input.is_empty()); - // Enter on an empty composer runs the card's highlighted menu - // entry while the card is up; the classification alone does not - // move focus (the event loop runs the entry). + // Enter on an empty composer with an untouched menu runs nothing: + // no entry is pre-selected, so a reflexive Enter at launch cannot + // create a worktree (founder live-test, 2026-09-02). let mut empty = launch_app(); + let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE); + assert_eq!(empty.launch.menu_selected, None); + assert_eq!( + handle_launch_composer_key(&mut empty, enter), + LaunchComposerKey::Consumed + ); + assert_eq!( + run_launch_menu_entry(&mut empty.launch, Locale::En), + LaunchAction::None + ); + assert!(empty.launch.worktree_input.is_none()); + assert!(empty.launch.composer_focus); + // Once the user has arrowed onto an entry, Enter runs it. assert_eq!( handle_launch_composer_key( &mut empty, - KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE) + KeyEvent::new(KeyCode::Down, KeyModifiers::NONE) ), + LaunchComposerKey::MenuNavigate(1) + ); + empty.launch.menu_selected = Some(0); + assert_eq!( + handle_launch_composer_key(&mut empty, enter), LaunchComposerKey::MenuRun ); - assert!(empty.launch.composer_focus); + // Esc unhighlights the menu instead of reaching the composer. + assert_eq!( + handle_launch_composer_key(&mut empty, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)), + LaunchComposerKey::Consumed + ); + assert_eq!(empty.launch.menu_selected, None); // Once the card has dissolved, empty-composer Enter is consumed: // there is no row to run and nothing to send. empty.launch.dissolve_started_ms = Some(0); assert_eq!( - handle_launch_composer_key( - &mut empty, - KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE) - ), + handle_launch_composer_key(&mut empty, enter), LaunchComposerKey::Consumed ); + // And Esc on the empty composer brings the card back. + assert_eq!( + handle_launch_composer_key(&mut empty, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)), + LaunchComposerKey::Consumed + ); + assert_eq!(empty.launch.dissolve_started_ms, None); + } + + #[test] + fn worktree_prompt_esc_returns_to_the_card() { + let mut app = launch_app(); + app.launch.worktree_available = true; + app.launch.menu_selected = Some(0); + assert_eq!( + run_launch_menu_entry(&mut app.launch, Locale::En), + LaunchAction::None + ); + assert!(app.launch.worktree_input.is_some()); + assert!(!app.launch.composer_focus); + assert_eq!( + handle_launch_key( + &mut app.launch, + KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), + Locale::En + ), + LaunchAction::None + ); + assert!(app.launch.worktree_input.is_none()); + assert!(app.launch.composer_focus); + assert!( + app.launch.dissolve_started_ms.is_none(), + "the card is still up" + ); } #[test] @@ -2630,7 +2700,7 @@ mod header_tests { assert!(filesystem_scope_notice(&app).is_none()); let line = header_line(&app, 120); assert!(!line.contains("files:"), "{line:?}"); - assert!(line.starts_with("Codewhale"), "{line:?}"); + assert!(line.starts_with("codewhale"), "{line:?}"); assert!(line.contains("work"), "{line:?}"); assert!(line.contains("ask"), "{line:?}"); } @@ -2881,8 +2951,8 @@ pub struct TidelineStartup<'a> { /// How far the launch card has dissolved, `[0.0 intact ..= 1.0 gone]`. /// Injected for the same determinism as `surface_progress`. pub card_dissolve: f32, - /// The card menu's highlighted entry. - pub menu_selected: usize, + /// The card menu's highlighted entry, if the user has arrowed onto one. + pub menu_selected: Option, /// The one migration notice above the composer, only when true. pub notice: Option, /// `model (effort) · permission` — the composer bottom rule's trailing @@ -2911,7 +2981,7 @@ impl<'a> TidelineStartup<'a> { mark: MarkTier::Braille, surface_progress: 1.0, card_dissolve: 0.0, - menu_selected: 0, + menu_selected: None, notice: None, composer_rule: None, branch: None, @@ -2935,7 +3005,7 @@ impl<'a> TidelineStartup<'a> { /// Set the card menu's highlighted entry. #[must_use] - pub fn menu_selected(mut self, selected: usize) -> Self { + pub fn menu_selected(mut self, selected: Option) -> Self { self.menu_selected = selected; self } @@ -3356,7 +3426,7 @@ fn render_launch_card( text_x, row, &Span::styled( - fit("Codewhale"), + fit("codewhale"), faded( Style::default() .fg(theme.accent_action) @@ -3367,7 +3437,7 @@ fn render_launch_card( ), ); let version = format!("v{}", startup.version); - let version_x = text_x + "Codewhale".width() as u16 + 1; + let version_x = text_x + "codewhale".width() as u16 + 1; if usize::from(version_x) + version.width() <= interior_w { set_span( buf, @@ -3392,10 +3462,11 @@ fn render_launch_card( row += 1; } - // The menu: Enter runs the highlighted entry; chords right-aligned. - let selected = startup.menu_selected % LAUNCH_MENU_ENTRIES; + // The menu: ↑/↓ highlight, Enter runs the highlighted entry; chords + // right-aligned. Nothing is highlighted until the user arrows. + let selected = startup.menu_selected.map(|s| s % LAUNCH_MENU_ENTRIES); for (index, (label, chord)) in entries.iter().enumerate().take(menu_rows as usize) { - let is_selected = index == selected; + let is_selected = selected == Some(index); let marker = startup.sym(crate::tui::glyphs::selection_marker(is_selected)); let marker_style = if is_selected { faded( diff --git a/crates/tui/src/tui/underwater/tideline_tests.rs b/crates/tui/src/tui/underwater/tideline_tests.rs index b8907fdd95..d271d72f9c 100644 --- a/crates/tui/src/tui/underwater/tideline_tests.rs +++ b/crates/tui/src/tui/underwater/tideline_tests.rs @@ -109,7 +109,7 @@ fn startup_matches_golden_at_the_40x12_terminal_floor() { // four-row dock all still fit. let text = draw(40, 10, &connected(&UI_THEME)); assert_matches_golden("startup_40x10", &text); - assert!(text.contains("Codewhale"), "{text}"); + assert!(text.contains("codewhale"), "{text}"); assert!(text.contains("New worktree"), "{text}"); assert!(text.contains("❯"), "the floor keeps the composer: {text}"); } @@ -128,14 +128,14 @@ fn startup_surfacing_midpoint_matches_its_golden() { "the midpoint frame differs from the still frame" ); // The header copy is already in place; only the mark is mid-surface. - assert!(text.contains("Codewhale v0.9.12"), "{text}"); + assert!(text.contains("codewhale v0.9.12"), "{text}"); } #[test] fn the_card_states_the_workspace_menu_and_mcp_news() { let text = draw(100, 30, &connected(&UI_THEME)); for fact in [ - "Codewhale v0.9.12", + "codewhale v0.9.12", // The top line owns the workspace truth now. "Hmbown/CodeWhale · main", // The card's announcement: only when true. @@ -159,7 +159,7 @@ fn the_card_states_the_workspace_menu_and_mcp_news() { "top line opens with the branch glyph: {first:?}" ); assert!( - !first.contains("Codewhale"), + !first.contains("codewhale"), "the wordmark left row 0: {first:?}" ); // Nothing from the old stage survives. @@ -202,7 +202,7 @@ fn startup_ascii_safe_drops_the_mark_and_every_wide_glyph() { "the branch glyph falls back to ASCII on row 0: {first:?}" ); assert!( - text.contains("Codewhale"), + text.contains("codewhale"), "the card keeps the wordmark: {text}" ); assert!( diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 1a9c7e03a2..8e86e1c44b 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -681,13 +681,10 @@ pub enum ViewEvent { value: String, persist: bool, }, - /// The canonical `/theme` picker owns theme and underwater treatment as - /// one selection. Keeping the pair in one event lets its command owner - /// preview, roll back, and persist both fields without an observable - /// half-selected Deepsea state. + /// The canonical `/theme` picker's selection. Preview, rollback, and + /// persist travel in one event so the theme never changes by half. ThemeSelectionUpdated { theme: String, - ocean_treatment: String, persist: bool, }, SubAgentsRefresh, @@ -2158,13 +2155,6 @@ impl ConfigView { scope: ConfigScope::Saved, facts: ConfigRowFacts::saved_setting(), }, - ConfigRow { - key: "ocean_treatment".to_string(), - value: settings.ocean_treatment.clone(), - editable: true, - scope: ConfigScope::Saved, - facts: ConfigRowFacts::saved_setting(), - }, ConfigRow { key: "focus_texture".to_string(), value: settings.focus_texture.clone(), diff --git a/crates/tui/src/tui/views/tideline_tests.rs b/crates/tui/src/tui/views/tideline_tests.rs index b7fa483df0..36cf1041b3 100644 --- a/crates/tui/src/tui/views/tideline_tests.rs +++ b/crates/tui/src/tui/views/tideline_tests.rs @@ -126,12 +126,12 @@ fn settings_strip_windows_to_the_selected_category_with_painted_hitboxes() { } #[test] -fn theme_list_shows_thirteen_themes_boxed_selection_and_motion_toggles() { +fn theme_list_shows_fourteen_themes_boxed_selection_and_motion_toggles() { let list = TidelineThemeList::new(&UI_THEME, 3).motion(false, true); let text = render_golden_text(30, 24, |buf| { render_tideline_theme_list(Rect::new(0, 0, 30, 24), buf, &list); }); - assert_eq!(SELECTABLE_THEMES.len(), 13, "4 mode rows + 9 presets"); + assert_eq!(SELECTABLE_THEMES.len(), 14, "4 mode rows + 10 presets"); for name in [ "System", "Terminal", @@ -141,9 +141,10 @@ fn theme_list_shows_thirteen_themes_boxed_selection_and_motion_toggles() { ] { assert!(text.contains(name), "missing {name}: {text}"); } - // Index 3 is the fourth mode row (Blue Stage Light). + // Index 3 is Blue Stage; Underwater sits between Terminal and Blue Stage. + assert!(text.contains("Underwater"), "{text}"); assert!( - text.contains("[ ✓ Blue Stage Light ]"), + text.contains("[ ✓ Blue Stage ]"), "selected row boxed with check: {text}" ); assert!(text.contains("MOTION (OPTIONAL)"), "{text}"); @@ -234,7 +235,7 @@ fn settings_rail_and_theme_list_hitboxes_match_painted_rows() { let mut buf = Buffer::empty(Rect::new(0, 0, w, h)); render_tideline_theme_list(form, &mut buf, &list); let boxes = tideline_theme_list_hitboxes(form, &list); - assert_eq!(boxes.len(), 15, "13 theme rows + 2 motion toggles"); + assert_eq!(boxes.len(), 16, "14 theme rows + 2 motion toggles"); for rect in &boxes { let cells: String = (rect.x..rect.x + rect.width) .map(|x| buf[(x, rect.y)].symbol().to_string()) diff --git a/crates/tui/src/tui/whales.rs b/crates/tui/src/tui/whales.rs index 30f587f437..b8b8a90e69 100644 --- a/crates/tui/src/tui/whales.rs +++ b/crates/tui/src/tui/whales.rs @@ -115,7 +115,7 @@ impl WhaleSpecies { Self::Echo => "Echo", Self::Keel => "Keel", Self::Lantern => "Lantern", - Self::Plain => "Codewhale", + Self::Plain => "codewhale", } } @@ -720,6 +720,6 @@ mod tests { } } assert_eq!(WhaleSpecies::Scout.name(), "Scout"); - assert_eq!(WhaleSpecies::Plain.name(), "Codewhale"); + assert_eq!(WhaleSpecies::Plain.name(), "codewhale"); } } diff --git a/crates/tui/src/tui/widgets/header.rs b/crates/tui/src/tui/widgets/header.rs index dc79a55098..3bc6b02ef8 100644 --- a/crates/tui/src/tui/widgets/header.rs +++ b/crates/tui/src/tui/widgets/header.rs @@ -50,7 +50,7 @@ pub fn header_status_indicator_frame( // Canonical mark, legacy whale opt-ins, and unknown values all land // on the static wordmark so the header never reintroduces an emoji // chip beside the operational chrome. - _ => return Some("Codewhale"), + _ => return Some("codewhale"), }; let elapsed_ms = turn_started_at .map(|t| t.elapsed().as_millis()) @@ -69,12 +69,12 @@ mod tests { for legacy in ["whale", "🐳", "🐋"] { assert_eq!( super::header_status_indicator_frame(None, legacy), - Some("Codewhale"), + Some("codewhale"), "legacy mode {legacy:?} must normalize to the cw mark" ); assert_eq!( super::header_status_indicator_frame(Some(std::time::Instant::now()), legacy), - Some("Codewhale"), + Some("codewhale"), "legacy mode {legacy:?} must stay static mid-turn" ); } @@ -84,11 +84,11 @@ mod tests { fn cw_indicator_is_static_wordmark() { assert_eq!( super::header_status_indicator_frame(None, "cw"), - Some("Codewhale") + Some("codewhale") ); assert_eq!( super::header_status_indicator_frame(Some(std::time::Instant::now()), "cw"), - Some("Codewhale") + Some("codewhale") ); } @@ -110,7 +110,7 @@ mod tests { #[test] fn unknown_indicator_mode_defaults_to_cw() { let frame = super::header_status_indicator_frame(None, "wahel-typo"); - assert_eq!(frame, Some("Codewhale")); + assert_eq!(frame, Some("codewhale")); } #[test] diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 58145be15d..2b1f4bbde8 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -129,9 +129,9 @@ impl ChatWidget { let content_area = area; let background = app.ui_theme.surface_bg; // The ordinary shell inherits its host/theme surface. Underwater life - // is earned by the explicit Deepsea treatment, never painted over a - // user's terminal simply because the app happens to be active. - let underwater_atmosphere = app.ocean_treatment.is_deepsea(); + // is earned by the underwater theme, never painted over a user's + // terminal simply because the app happens to be active. + let underwater_atmosphere = app.theme_id == crate::palette::ThemeId::Underwater; let ocean_ramp = underwater_atmosphere .then(|| crate::tui::ocean::OceanRamp::for_theme(&app.ui_theme)) .flatten(); @@ -186,6 +186,7 @@ impl ChatWidget { ); let life_presence_fixed = (life_presence * 1000.0).round().clamp(0.0, 1000.0) as u16; let ocean_column = ocean_ramp.map(|ramp| { + let context_percent = crate::tui::phase_strip::context_percent_from_app(app); crate::tui::ocean::OceanColumn::new( ramp, content_area, @@ -194,6 +195,7 @@ impl ChatWidget { phase, ocean_animated, life_presence_fixed, + context_percent, ) }); let fish_flee_elapsed_ms = underwater_motion_enabled @@ -4474,11 +4476,11 @@ mod tests { app.launch.visible = false; app.ui_locale = Locale::En; app.composer.vim_enabled = false; - // Most widget fixtures exercise the explicitly selected underwater - // scene. Production defaults to Flat/terminal-owned; keep tests that - // inspect fish and caustics intentional rather than coupled to that - // startup preference. - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; + // Most widget fixtures exercise the underwater theme's field. Other + // themes keep the terminal-owned shell; keep tests that inspect fish + // and caustics intentional rather than coupled to that choice. + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = palette::UNDERWATER_UI_THEME; app } @@ -6941,7 +6943,7 @@ mod tests { .collect::>() .join("\n"); - assert!(rendered.contains("Codewhale")); + assert!(rendered.contains("codewhale")); assert!(rendered.contains("/tmp/codewhale-test-workspace · no git · mcp 2")); assert!(rendered.contains("What do you want to accomplish?")); assert!(!rendered.contains("/workflow /goal /auto")); @@ -6978,10 +6980,10 @@ mod tests { let mut app = create_test_app(); // App::new reads persisted presentation settings. Other tests swap the // isolated settings home in parallel, so this visual contract must pin - // the treatment it is actually asserting instead of inheriting a - // transient Flat/Terminal choice from the process. - app.ui_theme = palette::UI_THEME; - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; + // the theme it is actually asserting instead of inheriting a transient + // non-underwater choice from the process. + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = palette::UNDERWATER_UI_THEME; app.low_motion = false; app.fancy_animations = true; app.workspace = PathBuf::from("codewhale-test-workspace"); @@ -7030,9 +7032,10 @@ mod tests { } #[test] - fn flat_treatment_keeps_theme_surface_without_ambient_life() { + fn terminal_owned_theme_keeps_theme_surface_without_ambient_life() { let mut app = create_test_app(); - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Flat; + app.theme_id = crate::palette::ThemeId::Whale; + app.ui_theme = palette::UI_THEME; app.low_motion = false; app.fancy_animations = true; let area = Rect::new(0, 0, 100, 20); @@ -7047,15 +7050,15 @@ mod tests { let rendered = buffer_text(&buf, area); assert!( !rendered.contains("><>") && !rendered.contains("<><"), - "flat must preserve a normal host-owned shell without decorative fish:\n{rendered}" + "terminal-owned themes must keep a normal shell without decorative fish:\n{rendered}" ); } #[test] - fn solarized_light_deepsea_keeps_canonical_surface_and_ambient_life() { + fn solarized_light_keeps_canonical_surface_without_a_field() { let mut app = create_test_app(); + app.theme_id = crate::palette::ThemeId::SolarizedLight; app.ui_theme = crate::palette::SOLARIZED_LIGHT_UI_THEME; - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; app.low_motion = false; app.fancy_animations = true; // The old cyan-tinted ramp produced the reported #e1e9da at row 16 @@ -7078,17 +7081,17 @@ mod tests { ); let rendered = buffer_text(&buf, area); assert!( - rendered.contains("><>") || rendered.contains("<><"), - "preserving the background must not remove ambient life:\n{rendered}" + !rendered.contains("><>") && !rendered.contains("<><"), + "a theme with no painted field earns no ambient life:\n{rendered}" ); } #[test] - fn solarized_light_custom_background_keeps_deepsea() { + fn underwater_custom_background_keeps_field_depth() { let mut app = create_test_app(); let custom = Color::Rgb(0x1a, 0x1b, 0x26); - app.ui_theme = crate::palette::SOLARIZED_LIGHT_UI_THEME.with_background_color(custom); - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = palette::UNDERWATER_UI_THEME.with_background_color(custom); let area = Rect::new(0, 0, 100, 30); let mut buf = Buffer::empty(area); @@ -7098,15 +7101,15 @@ mod tests { assert_ne!( buf[(0, 0)].bg, buf[(0, 29)].bg, - "custom Solarized Light backgrounds must retain Deepsea depth" + "custom backgrounds must not flatten the underwater field" ); } #[test] fn terminal_owned_background_stays_visually_quiet_without_deepsea() { let mut app = create_test_app(); + app.theme_id = crate::palette::ThemeId::Terminal; app.ui_theme = crate::palette::TERMINAL_UI_THEME; - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Flat; app.low_motion = false; app.fancy_animations = true; let area = Rect::new(0, 0, 100, 20); @@ -7186,8 +7189,8 @@ mod tests { #[test] fn reduced_motion_freezes_the_ocean_without_removing_depth() { let mut app = create_test_app(); - app.ui_theme = palette::UI_THEME; - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Deepsea; + app.theme_id = crate::palette::ThemeId::Underwater; + app.ui_theme = palette::UNDERWATER_UI_THEME; app.low_motion = true; app.fancy_animations = true; let area = Rect::new(0, 0, 100, 20); @@ -7579,8 +7582,8 @@ mod tests { fn chat_widget_uses_configured_surface_background() { let mut app = create_test_app(); let custom = ratatui::style::Color::Rgb(26, 27, 38); - app.ui_theme = app.ui_theme.with_background_color(custom); - app.ocean_treatment = crate::tui::ocean::OceanTreatment::Flat; + app.theme_id = crate::palette::ThemeId::Whale; + app.ui_theme = palette::UI_THEME.with_background_color(custom); app.add_message(HistoryCell::Assistant { content: "ready".to_string(), streaming: false, diff --git a/crates/tui/tests/cucumber/active_composer_pointer_pty.rs b/crates/tui/tests/cucumber/active_composer_pointer_pty.rs index f28fc37474..d6f2ef4aa8 100644 --- a/crates/tui/tests/cucumber/active_composer_pointer_pty.rs +++ b/crates/tui/tests/cucumber/active_composer_pointer_pty.rs @@ -301,7 +301,7 @@ fn assert_startup_contract(frame: &Frame, rows: u16, cols: u16, size: &str) { // real chords, and the focused composer. The posture bar and metrics // line appear only once a session exists, so `context` is NOT asserted // here any more (SHELL-DESIGN-20260901 Round 5). - for needle in ["Codewhale", "❯"] { + for needle in ["codewhale", "❯"] { assert!( text.contains(needle), "{size}: startup misses {needle:?}\n{}", @@ -340,7 +340,7 @@ fn assert_startup_contract(frame: &Frame, rows: u16, cols: u16, size: &str) { fn assert_live_shell_contract(frame: &Frame, cols: u16, size: &str) { let text = frame.text(); assert!( - text.contains("ctx "), + text.contains("no git"), "{size}: live shell misses the info line\n{}", frame.debug_dump() ); diff --git a/crates/tui/tests/cucumber/screen_mode_inline_pty.rs b/crates/tui/tests/cucumber/screen_mode_inline_pty.rs index 762d4448ca..e73bda9fb8 100644 --- a/crates/tui/tests/cucumber/screen_mode_inline_pty.rs +++ b/crates/tui/tests/cucumber/screen_mode_inline_pty.rs @@ -24,10 +24,11 @@ const ROWS: u16 = 24; const COLS: u16 = 80; const STARTUP_WAIT: Duration = Duration::from_secs(15); const SETTLE_WAIT: Duration = Duration::from_secs(5); -/// Stable proof the live shell repainted after a screen change: the context -/// meter's label on the info line, which every live-shell frame paints -/// regardless of composer state (and survives the wordmark's removal). -const LIVE_SHELL_SENTINEL: &str = "ctx "; +/// Stable proof the live shell repainted after a screen change: the composer +/// placeholder, which every live-shell frame paints in both screen modes. +/// The old `ctx` label no longer qualifies — it stays silent with no model +/// connected, and the workspace caption only paints in the inline stage. +const LIVE_SHELL_SENTINEL: &str = "Type a message"; #[test] fn inline_start_never_takes_the_alternate_screen_and_screen_commands_switch_it() {