From 5dd0c075262d600acba0564ccf672cb32e3a813f Mon Sep 17 00:00:00 2001 From: gajendraxdev Date: Fri, 17 Jul 2026 01:21:24 +0530 Subject: [PATCH 1/5] fix(ai,vault): privacy labels, provider config, and missing-key UX Hide vault host addresses when the privacy toggle is off, stop partial AI settings from falling back to Ollama, keep Agent mode from flipping to Ask on greetings, and surface missing API keys in-chat with a Settings deep link. Bump to 2.22.2 and document in CHANGELOG. --- CHANGELOG.md | 11 +++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/ai/config.rs | 40 +++++--- src-tauri/src/ai/types.rs | 33 ++++++- src-tauri/tauri.conf.json | 2 +- src/components/ai/AiChatMessage.tsx | 11 ++- src/components/ai/AiSetupErrorCard.tsx | 44 +++++++++ src/components/ai/AiSidebar.tsx | 94 ++++++++++++------- src/components/ai/ConversationThread.tsx | 9 +- src/components/ai/aiSetupErrors.ts | 67 +++++++++++++ src/components/ai/sidebarSubmit.ts | 5 - src/components/modals/useAutoVault.ts | 8 +- src/components/settings/SettingsModal.tsx | 9 ++ .../tabs/vault/CredentialHistoryModal.tsx | 20 +++- .../tabs/vault/ManageAssignmentsModal.tsx | 19 +++- .../tabs/vault/RestoreConflictModal.tsx | 5 +- .../tabs/vault/RotateCredentialModal.tsx | 9 +- .../tabs/vault/VaultCredentialDetailModal.tsx | 34 ++++--- .../settings/tabs/vault/VaultItemsPanel.tsx | 23 +++-- .../vault/hooks/useAssignCredentialModal.ts | 6 +- .../tabs/vault/hooks/useHistoryModal.ts | 8 +- .../tabs/vault/hooks/useVaultPanelActions.ts | 11 ++- .../settings/tabs/vault/privacyLabel.ts | 13 +++ .../connections/domain/connectionDisplay.ts | 59 ++++++++++++ src/store/settingsSlice.ts | 35 +++++-- tests/connectionDisplay.test.mjs | 45 +++++++++ tests/sidebarSubmit.test.mjs | 68 ++++++++++++-- 29 files changed, 576 insertions(+), 118 deletions(-) create mode 100644 src/components/ai/AiSetupErrorCard.tsx create mode 100644 src/components/ai/aiSetupErrors.ts create mode 100644 src/components/settings/tabs/vault/privacyLabel.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 18927ddd..89f3c413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to Zync are documented in this file. The format is based on ## [Unreleased] +## [2.22.2] - 2026-07-16 + +### Fixed +- **Vault list privacy**: Credential labels, assignment/detail/history/restore modals, and related confirms/toasts respect **Settings → General → Show host addresses in lists**, so demos no longer leak IPs from vault UI. New secured-key labels avoid embedding `user@ip`. +- **AI provider selection**: Partial `settings.ai` (e.g. Mistral without `enabled`) no longer fails parse and silently falls back to Ollama. Serde defaults, soft recovery on read, and full AI object persistence when changing provider/model. +- **Agent mode routing**: Short greetings no longer force Ask mode while Agent is selected. +- **Missing API key UX**: Cloud providers fail fast in the AI sidebar with a clear in-chat message and **Open Settings → AI** deep link (all BYOK providers, not only Mistral). + +### Changed +- Shared privacy label helpers and setup-error copy; AI settings rollback is key-scoped so concurrent AI updates are preserved. + ## [2.22.1] - 2026-07-14 ### Fixed diff --git a/package.json b/package.json index cfeacc48..70b5bbc0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "zync", "private": true, - "version": "2.22.1", + "version": "2.22.2", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 64b7aa78..1a03fb06 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7975,7 +7975,7 @@ dependencies = [ [[package]] name = "zync" -version = "2.22.1" +version = "2.22.2" dependencies = [ "anyhow", "argon2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5334b500..876d6d13 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zync" -version = "2.22.1" +version = "2.22.2" description = "A modern SSH client" authors = ["Gajendra"] edition = "2021" diff --git a/src-tauri/src/ai/config.rs b/src-tauri/src/ai/config.rs index 63af7082..1b64496a 100644 --- a/src-tauri/src/ai/config.rs +++ b/src-tauri/src/ai/config.rs @@ -29,13 +29,7 @@ fn merge_secret_keys(app: &AppHandle, mut config: AiConfig) -> AiConfig { } fn default_ai_config() -> AiConfig { - AiConfig { - provider: "ollama".to_string(), - keys: None, - model: None, - ollama_url: Some("http://localhost:11434".to_string()), - enabled: true, - } + AiConfig::default() } pub fn read_ai_config(app: &AppHandle) -> AiConfig { @@ -44,10 +38,34 @@ pub fn read_ai_config(app: &AppHandle) -> AiConfig { if let Some(ai) = settings.get("ai") { match serde_json::from_value::(ai.clone()) { Ok(config) => return merge_secret_keys(app, config), - #[cfg(debug_assertions)] - Err(e) => eprintln!("[zync/ai] Failed to parse AI config from effective settings: {e}"), - #[cfg(not(debug_assertions))] - Err(_) => {} + Err(e) => { + // Soft-parse: fill missing fields from defaults instead of + // throwing away a valid provider selection (e.g. mistral without `enabled`). + #[cfg(debug_assertions)] + eprintln!( + "[zync/ai] Partial AI config parse failed ({e}); merging with defaults" + ); + let defaults = serde_json::to_value(AiConfig::default()) + .unwrap_or_else(|_| serde_json::json!({})); + // Only apply non-null overlay fields so explicit JSON null + // cannot wipe valid defaults (e.g. "enabled": null). + let merged = match (defaults, ai.clone()) { + (serde_json::Value::Object(mut base), serde_json::Value::Object(overlay)) => { + for (k, v) in overlay { + if !v.is_null() { + base.insert(k, v); + } + } + serde_json::Value::Object(base) + } + (_, overlay) => overlay, + }; + if let Ok(config) = serde_json::from_value::(merged) { + return merge_secret_keys(app, config); + } + #[cfg(debug_assertions)] + eprintln!("[zync/ai] Failed to recover AI config after merge with defaults"); + } } } else { #[cfg(debug_assertions)] diff --git a/src-tauri/src/ai/types.rs b/src-tauri/src/ai/types.rs index 13ea36e8..1b09a955 100644 --- a/src-tauri/src/ai/types.rs +++ b/src-tauri/src/ai/types.rs @@ -6,16 +6,47 @@ use serde::{Deserialize, Serialize}; use crate::ai::AiTranslateResponse; -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +fn default_ai_provider() -> String { + "ollama".to_string() +} + +fn default_ai_enabled() -> bool { + true +} + +fn default_ollama_url() -> Option { + Some("http://localhost:11434".to_string()) +} + +/// Partial `settings.ai` objects (e.g. only `{ provider, model }`) must still deserialize. +/// Missing `enabled` previously failed parse and silently fell back to Ollama defaults. +#[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AiConfig { + #[serde(default = "default_ai_provider")] pub provider: String, + #[serde(default)] pub keys: Option>, + #[serde(default)] pub model: Option, + #[serde(default = "default_ollama_url")] pub ollama_url: Option, + #[serde(default = "default_ai_enabled")] pub enabled: bool, } +impl Default for AiConfig { + fn default() -> Self { + Self { + provider: default_ai_provider(), + keys: None, + model: None, + ollama_url: default_ollama_url(), + enabled: default_ai_enabled(), + } + } +} + impl AiConfig { pub(crate) fn api_key(&self) -> Option<&str> { self.keys diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 30672780..ceb7dc0a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "zync", - "version": "2.22.1", + "version": "2.22.2", "identifier": "zync", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/ai/AiChatMessage.tsx b/src/components/ai/AiChatMessage.tsx index e807c0d8..a2453211 100644 --- a/src/components/ai/AiChatMessage.tsx +++ b/src/components/ai/AiChatMessage.tsx @@ -1,8 +1,11 @@ import { memo, useCallback } from 'react'; -import { Copy, Play, ShieldCheck, ShieldAlert, AlertTriangle, Terminal, User, AlertCircle } from 'lucide-react'; +import { Copy, Play, ShieldCheck, ShieldAlert, AlertTriangle, Terminal, User } from 'lucide-react'; import { AgentIcon } from './AgentIcon'; import { cn } from '../../lib/utils'; import type { AiDisplayEntry } from '../../ai/types/common'; +import { AiSetupErrorCard } from './AiSetupErrorCard'; +import { useAppStore } from '../../store/useAppStore'; +import type { ProviderValue } from './providerCatalog'; interface AiChatMessageProps { entry: AiDisplayEntry; @@ -76,6 +79,7 @@ function FormattedText({ text }: { text: string }) { export const AiChatMessage = memo(function AiChatMessage({ entry, onRunCommand }: AiChatMessageProps) { const { query, result, error, contextSnapshot, timestamp } = entry; + const aiProvider = useAppStore((s) => s.settings.ai?.provider) as ProviderValue | undefined; const time = new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); return ( @@ -108,10 +112,7 @@ export const AiChatMessage = memo(function AiChatMessage({ entry, onRunCommand }
{error ? ( -
- -

{error}

-
+ ) : result ? ( <> {/* Main Answer (for conversational / Q&A responses) */} diff --git a/src/components/ai/AiSetupErrorCard.tsx b/src/components/ai/AiSetupErrorCard.tsx new file mode 100644 index 00000000..3f83332e --- /dev/null +++ b/src/components/ai/AiSetupErrorCard.tsx @@ -0,0 +1,44 @@ +import { AlertCircle, Settings } from 'lucide-react'; +import { friendlyAiError } from './aiSetupErrors'; +import type { ProviderValue } from './providerCatalog'; +import { useAppStore } from '../../store/useAppStore'; + +interface AiSetupErrorCardProps { + message: string; + provider?: ProviderValue; + className?: string; +} + +/** + * Chat-inline setup / auth error with optional one-click open to Settings → AI. + */ +export function AiSetupErrorCard({ message, provider, className }: AiSetupErrorCardProps) { + const openSettings = useAppStore((s) => s.openSettings); + const settingsProvider = useAppStore((s) => s.settings.ai?.provider) as ProviderValue | undefined; + const resolvedProvider = provider ?? settingsProvider; + const { text, showSettingsCta } = friendlyAiError(message, resolvedProvider); + + return ( +
+
+ +

{text}

+
+ {showSettingsCta && ( + + )} +
+ ); +} diff --git a/src/components/ai/AiSidebar.tsx b/src/components/ai/AiSidebar.tsx index f68762c8..19f6882e 100644 --- a/src/components/ai/AiSidebar.tsx +++ b/src/components/ai/AiSidebar.tsx @@ -17,10 +17,16 @@ import { } from './providerCatalog'; import { useAiProviderModels } from './useAiProviderModels'; import { collectAiRequestContext } from '../../lib/aiContext'; -import { startAgentRun, stopAgentRun, clearBrainSessions } from '../../ai/services/aiClient'; +import { getSavedProviderKey, startAgentRun, stopAgentRun, clearBrainSessions } from '../../ai/services/aiClient'; import { useAgentRunStore } from '../../ai/store/agentRunStore'; import { - shouldTreatAgentInputAsAsk, + formatMissingApiKeyMessage, + NO_MODEL_SELECTED_MESSAGE, + OLLAMA_NO_MODEL_MESSAGE, + OLLAMA_NOT_RUNNING_MESSAGE, + providerRequiresApiKey, +} from './aiSetupErrors'; +import { submitAgentGoal, submitAskQuery, } from './sidebarSubmit'; @@ -302,6 +308,33 @@ export function AiSidebar({ connectionId, activeTermId: activeTermIdProp, onRunC } }, [activeRunId, showToast]); + /** Post a setup/auth error into the visible chat thread (Ask or Agent). */ + const postSetupErrorInChat = useCallback((userQuery: string, setupMessage: string) => { + if (isAgentMode) { + // Show the user turn + error in the agent thread without starting a network run. + const localRunId = crypto.randomUUID(); + agentAct().startRun(agentScope, localRunId, userQuery); + agentAct().addError(agentScope, setupMessage); + agentAct().endRun(agentScope); + return; + } + + if (connectionId) { + addToDisplayHistory(connectionId, { + id: crypto.randomUUID(), + query: userQuery, + result: null, + error: setupMessage, + contextSnapshot: attachedContext?.content ?? null, + timestamp: Date.now(), + }); + return; + } + + // No host selected — still surface the message. + showToast('warning', setupMessage); + }, [isAgentMode, agentScope, connectionId, attachedContext, addToDisplayHistory, showToast]); + const handleSubmit = useCallback(async () => { const trimmed = query.trim(); if (!trimmed || isLoading || agentRunning) return; @@ -309,35 +342,35 @@ export function AiSidebar({ connectionId, activeTermId: activeTermIdProp, onRunC if (providerNeedsSetup) { const setupMessage = activeProviderValue === 'ollama' - ? (!ollamaAvailable - ? 'Ollama is not running. Start Ollama or switch to another provider.' - : 'No Ollama model found. Pull a model (for example: ollama pull llama3.2) or switch provider.') - : 'No model selected for the current provider. Please select a model and try again.'; - - if (isAgentMode) { - agentAct().addError(agentScope, setupMessage); - } else if (connectionId) { - addToDisplayHistory(connectionId, { - id: crypto.randomUUID(), - query: trimmed, - result: null, - error: setupMessage, - contextSnapshot: attachedContext?.content ?? null, - timestamp: Date.now(), - }); - } else { - showToast('warning', setupMessage); - } + ? (!ollamaAvailable ? OLLAMA_NOT_RUNNING_MESSAGE : OLLAMA_NO_MODEL_MESSAGE) + : NO_MODEL_SELECTED_MESSAGE; + + postSetupErrorInChat(trimmed, setupMessage); + setQuery(''); + if (inputRef.current) inputRef.current.style.height = 'auto'; return; } - if (isAgentMode) { - if (shouldTreatAgentInputAsAsk(trimmed)) { - setAiMode('ask'); - await handleSubmitAsk(trimmed); - } else { - await handleSubmitAgent(trimmed); + // BYOK providers: fail fast in-chat with a Settings CTA instead of a raw network error. + if (providerRequiresApiKey(activeProviderValue)) { + try { + const key = await getSavedProviderKey(activeProviderValue); + if (!key?.trim()) { + postSetupErrorInChat(trimmed, formatMissingApiKeyMessage(activeProviderValue)); + setQuery(''); + if (inputRef.current) inputRef.current.style.height = 'auto'; + return; + } + } catch (error) { + console.error('[AiSidebar] Failed to check provider API key', error); + // Fall through to normal submit; backend will still return a clear key error. } + } + + // Agent mode always uses the agent path — do not silently flip to Ask + // for greetings (that made it look like the wrong mode was responding). + if (isAgentMode) { + await handleSubmitAgent(trimmed); } else { await handleSubmitAsk(trimmed); } @@ -350,14 +383,9 @@ export function AiSidebar({ connectionId, activeTermId: activeTermIdProp, onRunC providerNeedsSetup, activeProviderValue, ollamaAvailable, - agentScope, - connectionId, - attachedContext, - addToDisplayHistory, - showToast, + postSetupErrorInChat, handleSubmitAgent, handleSubmitAsk, - setAiMode, ]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { diff --git a/src/components/ai/ConversationThread.tsx b/src/components/ai/ConversationThread.tsx index 37003cc2..782f4f7d 100644 --- a/src/components/ai/ConversationThread.tsx +++ b/src/components/ai/ConversationThread.tsx @@ -20,6 +20,7 @@ import { useAgentRunStore } from '../../ai/store/agentRunStore'; import { ToolCallBlock } from './ToolCallBlock'; import { CheckpointBlock } from './CheckpointBlock'; import { PlanBubble } from './PlanBubble'; +import { AiSetupErrorCard } from './AiSetupErrorCard'; import { respondToCheckpoint, whitelistCommand } from '../../ai/services/aiClient'; import type { AgentThinkingEvent, @@ -228,9 +229,11 @@ function DoneBubble({ success, summary, actions = [], sessionPath }: { success: function ErrorBubble({ message }: { message: string }) { return ( -
- -

{message}

+
+
); } diff --git a/src/components/ai/aiSetupErrors.ts b/src/components/ai/aiSetupErrors.ts new file mode 100644 index 00000000..7df7b3f0 --- /dev/null +++ b/src/components/ai/aiSetupErrors.ts @@ -0,0 +1,67 @@ +import type { ProviderValue } from './providerCatalog'; +import { getProviderOption } from './providerCatalog'; + +/** Shared setup-copy for Ollama / model readiness (sidebar + error mapping). */ +export const OLLAMA_NOT_RUNNING_MESSAGE = + 'Ollama is not running. Start Ollama, or switch to another provider in Settings → AI.'; + +export const OLLAMA_NO_MODEL_MESSAGE = + 'No Ollama model found. Pull a model (for example: ollama pull llama3.2) or switch provider in Settings → AI.'; + +export const NO_MODEL_SELECTED_MESSAGE = + 'No model selected for the current provider. Pick a model, or open Settings → AI.'; + +export function providerRequiresApiKey(provider: ProviderValue): boolean { + return provider !== 'ollama'; +} + +export function formatMissingApiKeyMessage(provider: ProviderValue): string { + const label = getProviderOption(provider).label; + return `${label} API key is not set. Add your key in Settings → AI, then try again.`; +} + +export interface FriendlyAiError { + text: string; + showSettingsCta: boolean; +} + +/** Map backend/provider errors into chat-friendly copy + whether to offer Settings navigation. */ +export function friendlyAiError( + message: string, + provider?: ProviderValue, +): FriendlyAiError { + const trimmed = (message ?? '').trim(); + if (!trimmed) { + return { text: 'Something went wrong. Please try again.', showSettingsCta: false }; + } + + if (/API key not configured/i.test(trimmed)) { + return { + text: provider + ? formatMissingApiKeyMessage(provider) + : 'API key is not set. Add your key in Settings → AI, then try again.', + showSettingsCta: true, + }; + } + + if (/Invalid .+ API key/i.test(trimmed)) { + const label = provider ? getProviderOption(provider).label : 'provider'; + return { + text: `That ${label} API key was rejected. Update it in Settings → AI and try again.`, + showSettingsCta: true, + }; + } + + if (/Ollama not running|ollama serve/i.test(trimmed)) { + return { + text: OLLAMA_NOT_RUNNING_MESSAGE, + showSettingsCta: true, + }; + } + + if (/Settings\s*->\s*AI|Settings\s*→\s*AI/i.test(trimmed)) { + return { text: trimmed.replace(/Settings\s*->\s*AI/gi, 'Settings → AI'), showSettingsCta: true }; + } + + return { text: trimmed, showSettingsCta: false }; +} diff --git a/src/components/ai/sidebarSubmit.ts b/src/components/ai/sidebarSubmit.ts index b1f938c4..7923bade 100644 --- a/src/components/ai/sidebarSubmit.ts +++ b/src/components/ai/sidebarSubmit.ts @@ -15,11 +15,6 @@ export interface AgentRunActions { endRun: (scope: string) => void; } -export function shouldTreatAgentInputAsAsk(query: string): boolean { - return /^(hi|hello|hey|sup|yo|greetings|howdy|good morning|good afternoon|good evening|thanks|thank you)\b/i.test(query) - && query.length < 35; -} - export async function submitAskQuery(params: { trimmed: string; connectionId: string | null; diff --git a/src/components/modals/useAutoVault.ts b/src/components/modals/useAutoVault.ts index 1f82f569..cee02948 100644 --- a/src/components/modals/useAutoVault.ts +++ b/src/components/modals/useAutoVault.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useAppStore, Connection } from '../../store/useAppStore'; import { useVaultStore } from '../../vault/useVaultStore'; import { vaultIpc } from '../../vault/ipc'; -import { buildConnectionSavePayload } from '../../features/connections/domain'; +import { buildConnectionSavePayload, buildDefaultKeyVaultLabel } from '../../features/connections/domain'; import { ToastType } from '../../store/toastSlice'; interface UseAutoVaultOptions { @@ -44,7 +44,11 @@ export function useAutoVault({ setKeyVaultLabel(''); }, [isOpen]); - const defaultKeyVaultLabel = `${formData.name || formData.host || 'credential'} key (${formData.username || 'user'}@${formData.host || 'host'})`; + const defaultKeyVaultLabel = buildDefaultKeyVaultLabel({ + name: formData.name, + host: formData.host, + username: formData.username, + }); const effectiveKeyVaultLabel = keyVaultLabel.trim() || defaultKeyVaultLabel; const keyVaultLabelConflict = vaultStatus?.status === 'unlocked' && authMethod === 'key' diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx index 87ebc368..0e05a83b 100644 --- a/src/components/settings/SettingsModal.tsx +++ b/src/components/settings/SettingsModal.tsx @@ -40,6 +40,8 @@ const BUILTIN_ICON_THEME_COUNT = 2; // VSCode Icons + Lucide export function SettingsModal({ isOpen, onClose }: SettingsModalProps) { const settings = useAppStore(state => state.settings); + const settingsFocusTab = useAppStore(state => state.settingsFocusTab); + const clearSettingsFocusTab = useAppStore(state => state.clearSettingsFocusTab); const updateSettings = useAppStore(state => state.updateSettings); const updateAiSettings = useAppStore(state => state.updateAiSettings); const updateTerminalSettings = useAppStore(state => state.updateTerminalSettings); @@ -83,6 +85,13 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) { const [isTransitioning, setIsTransitioning] = useState(false); const [wslDistros, setWslDistros] = useState([]); + // Deep-link from chat / command palette: open Settings already focused on a tab. + useEffect(() => { + if (!isOpen || !settingsFocusTab) return; + setActiveTab(settingsFocusTab); + clearSettingsFocusTab(); + }, [isOpen, settingsFocusTab, clearSettingsFocusTab]); + // Global Update State const updateStatus = useAppStore(state => state.updateStatus); const updateInfo = useAppStore(state => state.updateInfo); diff --git a/src/components/settings/tabs/vault/CredentialHistoryModal.tsx b/src/components/settings/tabs/vault/CredentialHistoryModal.tsx index 0b132bab..a0596987 100644 --- a/src/components/settings/tabs/vault/CredentialHistoryModal.tsx +++ b/src/components/settings/tabs/vault/CredentialHistoryModal.tsx @@ -1,6 +1,8 @@ import { History, RotateCcw } from 'lucide-react'; import { useMemo } from 'react'; import type { RevisionMeta, VaultItem } from '../../../../vault/ipc'; +import { formatPrivacyAwareLabel } from '../../../../features/connections/domain/connectionDisplay'; +import { useShowHostAddressesInLists } from '../../../../features/connections/presentation/useConnectionDisplayLabels'; import { Button } from '../../../ui/Button'; import { Modal } from '../../../ui/Modal'; @@ -43,13 +45,18 @@ export function CredentialHistoryModal({ onClose, onRestore, }: CredentialHistoryModalProps) { + const showHostAddressesInLists = useShowHostAddressesInLists(); // Memoize the reversed array so we don't allocate on every render. const sortedHistory = useMemo(() => [...history].reverse(), [history]); + const displayItemLabel = item + ? formatPrivacyAwareLabel(item.label, showHostAddressesInLists) + : null; + return ( @@ -92,14 +99,16 @@ export function CredentialHistoryModal({ Previous revisions — newest first

- {sortedHistory.map((rev) => ( + {sortedHistory.map((rev) => { + const revDisplayLabel = formatPrivacyAwareLabel(rev.label, showHostAddressesInLists); + return (

- {rev.label} + {revDisplayLabel} rev {rev.revision} @@ -115,13 +124,14 @@ export function CredentialHistoryModal({ disabled={isRestoring} className="shrink-0 h-7 gap-1 px-2 text-[11px] md:opacity-0 md:group-hover:opacity-100 focus:opacity-100 transition-opacity" title={`Restore revision ${rev.revision}`} - aria-label={`Restore revision ${rev.revision} of ${rev.label}`} + aria-label={`Restore revision ${rev.revision} of ${revDisplayLabel}`} > Restore

- ))} + ); + })}
)} diff --git a/src/components/settings/tabs/vault/ManageAssignmentsModal.tsx b/src/components/settings/tabs/vault/ManageAssignmentsModal.tsx index 80bb429e..ed618343 100644 --- a/src/components/settings/tabs/vault/ManageAssignmentsModal.tsx +++ b/src/components/settings/tabs/vault/ManageAssignmentsModal.tsx @@ -1,4 +1,9 @@ import type { Connection } from '../../../../features/connections/domain/types'; +import { + formatPrivacyAwareLabel, + getConnectionDisplayLabels, +} from '../../../../features/connections/domain/connectionDisplay'; +import { useShowHostAddressesInLists } from '../../../../features/connections/presentation/useConnectionDisplayLabels'; import { Button } from '../../../ui/Button'; import { Input } from '../../../ui/Input'; import { Modal } from '../../../ui/Modal'; @@ -32,11 +37,16 @@ export function ManageAssignmentsModal({ onClear, onSubmit, }: ManageAssignmentsModalProps) { + const showHostAddressesInLists = useShowHostAddressesInLists(); + const displayItemLabel = itemLabel + ? formatPrivacyAwareLabel(itemLabel, showHostAddressesInLists) + : null; + return ( @@ -56,6 +66,7 @@ export function ManageAssignmentsModal({
) : filteredConnections.map((connection) => { const checked = selectedAssignConnectionIds.includes(connection.id); + const labels = getConnectionDisplayLabels(connection, showHostAddressesInLists); return (