From a776efb06a2f5ca6f68e5ca5530f6975b88c2ebb Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 27 Sep 2026 12:57:32 -0500 Subject: [PATCH] fix(desktop): show saved key previews without the backend's redaction sentinel Fixes #124378 Co-authored-by: Adolan <94890352+Adolanium@users.noreply.github.com> --- apps/desktop/src/app/messaging/index.tsx | 3 ++- apps/desktop/src/app/settings/credential-key-ui.tsx | 4 ++-- apps/desktop/src/app/settings/helpers.test.ts | 11 +++++++++++ apps/desktop/src/app/settings/helpers.ts | 13 +++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/messaging/index.tsx b/apps/desktop/src/app/messaging/index.tsx index 4354e1e574..fca0fab6e7 100644 --- a/apps/desktop/src/app/messaging/index.tsx +++ b/apps/desktop/src/app/messaging/index.tsx @@ -38,6 +38,7 @@ import { useRouteEnumParam } from '../hooks/use-route-enum-param' import { DetailColumn, ListColumn, MasterDetail } from '../master-detail' import { PageSearchShell } from '../page-search-shell' import { CREDENTIAL_CONTROL_CLASS } from '../settings/credential-key-ui' +import { credentialPreview } from '../settings/helpers' import { ListRow } from '../settings/primitives' import { SettingsProfileScope } from '../settings/profile-scope' import type { SetStatusbarItemGroup } from '../shell/statusbar-controls' @@ -998,7 +999,7 @@ function MessagingField({ className={CREDENTIAL_CONTROL_CLASS} id={fieldId} onChange={event => onEdit(field.key, event.target.value)} - placeholder={field.is_set ? field.redacted_value || m.replaceValue : copy.placeholder} + placeholder={field.is_set ? credentialPreview(field.redacted_value) || m.replaceValue : copy.placeholder} type={field.is_password ? 'password' : 'text'} value={edits[field.key] || ''} /> diff --git a/apps/desktop/src/app/settings/credential-key-ui.tsx b/apps/desktop/src/app/settings/credential-key-ui.tsx index cd148fe07b..1637cd89cf 100644 --- a/apps/desktop/src/app/settings/credential-key-ui.tsx +++ b/apps/desktop/src/app/settings/credential-key-ui.tsx @@ -9,7 +9,7 @@ import { cn } from '@/lib/utils' import type { EnvVarInfo } from '@/types/hermes' import { CONTROL_TEXT } from './constants' -import { prettyName, withoutKey } from './helpers' +import { credentialPreview, prettyName, withoutKey } from './helpers' import { LIST_ROW_COLUMNS, ListRow } from './primitives' import type { EnvRowProps } from './types' @@ -68,7 +68,7 @@ export function KeyField({ const draft = edits[editKey] ?? '' const dirty = draft.trim().length > 0 const busy = saving === varKey - const masked = info.redacted_value ?? '••••••••' + const masked = credentialPreview(info.redacted_value) ?? '••••••••' const startEdit = () => setEdits(c => ({ ...c, [editKey]: '' })) const cancel = () => setEdits(c => withoutKey(c, editKey)) const update = (e: ChangeEvent) => setEdits(c => ({ ...c, [editKey]: e.target.value })) diff --git a/apps/desktop/src/app/settings/helpers.test.ts b/apps/desktop/src/app/settings/helpers.test.ts index 3ba7633016..a8a61db1de 100644 --- a/apps/desktop/src/app/settings/helpers.test.ts +++ b/apps/desktop/src/app/settings/helpers.test.ts @@ -6,6 +6,7 @@ import { BUILTIN_PERSONALITIES } from './constants' import { defineFieldCopy, fieldCopyForSchemaKey, schemaKeyToFieldCopyKey } from './field-copy' import { clearsEnabledToolsets, + credentialPreview, diffConfig, enumOptionsFor, getNested, @@ -445,3 +446,13 @@ describe('settings helpers', () => { }) }) }) + +describe('credentialPreview', () => { + it('unwraps the backend preview sentinel and masks label-less forms', () => { + expect(credentialPreview('«redacted:sk-h...JPJ8»')).toBe('sk-h...JPJ8') + expect(credentialPreview('«redacted-secret»')).toBe('••••••••') + expect(credentialPreview('«redacted-vault-secret»')).toBe('••••••••') + expect(credentialPreview('sk-h...JPJ8')).toBe('sk-h...JPJ8') + expect(credentialPreview(null)).toBeNull() + }) +}) diff --git a/apps/desktop/src/app/settings/helpers.ts b/apps/desktop/src/app/settings/helpers.ts index 850d4b7172..a792829b4e 100644 --- a/apps/desktop/src/app/settings/helpers.ts +++ b/apps/desktop/src/app/settings/helpers.ts @@ -26,6 +26,19 @@ export const withoutKey = (record: Record, key: string) => { export const redactedValue = (v: string) => (v.length <= 8 ? '••••' : `${v.slice(0, 4)}...${v.slice(-4)}`) +// The backend wraps stored-key previews in a write-guard sentinel +// (hermes_cli/web_routers/_common.redacted_credential_preview): show the inner +// preview, and a plain mask for the label-less forms. +export const credentialPreview = (value: null | string | undefined): null | string => { + if (!value?.startsWith('«redacted')) { + return value || null + } + + const inner = /^«redacted:(.+)»$/.exec(value)?.[1]?.trim() + + return inner || '••••••••' +} + // Longest-prefix match so a more specific group like ``MINIMAX_CN_`` is // chosen over its shorter parent ``MINIMAX_``. Falls back to the bucket // "Other" used by the Keys settings view for un-grouped env vars.