diff --git a/apps/desktop/src/components/redis/RedisValueViewer.vue b/apps/desktop/src/components/redis/RedisValueViewer.vue index c1d2cb29dd..9ffa7f67ea 100644 --- a/apps/desktop/src/components/redis/RedisValueViewer.vue +++ b/apps/desktop/src/components/redis/RedisValueViewer.vue @@ -28,14 +28,16 @@ import { copyToClipboard } from "@/lib/common/clipboard"; import { formatTtl } from "@/lib/common/ttlFormat"; import { computeDisplayTtl, computeTtlCountdownTick, computeTtlCountdownValue, computeTtlForExpiryEdit, DEFAULT_REDIS_AUTO_REFRESH_INTERVAL_SECONDS, normalizeRedisAutoRefreshInterval } from "@/lib/redis/redisAutoRefresh"; import { + autoRedisValueFormat, canRenderRedisValueFormat, canEditRedisMemberDetail, decodeRedisBlob, + detectedRedisStructuredCodec, formatRedisMemberDetail, getRedisMemberSelectionKey, isRedisBlob, + isRedisJsonContainerValue, parseRedisJsonDetail, - preferredRedisValueFormat, REDIS_VALUE_FORMAT_DISPLAY_ORDER, redisBlobText, redisCollectionPageItems, @@ -57,6 +59,7 @@ import { REDIS_VALUE_CODEC_ORDER, type RedisCollectionItem, type RedisHashRowCopyTarget, + type RedisMemberDetail, type RedisValueCodec, type RedisValueFormat, } from "@/lib/redis/redisValuePresentation"; @@ -193,10 +196,10 @@ const isResizingHashColumns = ref(false); const zsetTableRef = ref(null); const zsetScoreWidth = ref(220); const isResizingZsetColumns = ref(false); -const stringValueView = ref(readPreferredRedisValueFormat()); -const memberValueView = ref(readPreferredRedisValueFormat()); -const stringValueCodec = ref(readPreferredRedisValueCodec()); -const memberValueCodec = ref(readPreferredRedisValueCodec()); +const stringValueView = ref(readStoredRedisValueFormat() ?? "utf8"); +const memberValueView = ref(readStoredRedisValueFormat() ?? "utf8"); +const stringValueCodec = ref(readStoredRedisValueCodec() ?? "none"); +const memberValueCodec = ref(readStoredRedisValueCodec() ?? "none"); const redisJsonWordWrap = ref(readRedisJsonWordWrap()); const redisJsonUnicodeMode = ref(readRedisJsonUnicodeMode()); const redisJsonDecoded = computed(() => redisJsonUnicodeMode.value === "decoded"); @@ -479,6 +482,40 @@ watch([memberValueCodec, selectedMemberRaw], ([codec]) => { const stringGzipBadge = computed(() => (stringBlob.value ? isGzipMagic(decodeRedisBlob(stringBlob.value)) : false)); const memberGzipBadge = computed(() => (isRedisBlob(selectedMemberRaw.value) ? isGzipMagic(decodeRedisBlob(selectedMemberRaw.value)) : false)); +/** + * Badge label for the detected payload representation (issue #10922): JSON + * containers and deterministic binary formats the user would otherwise see + * as an opaque blob. Plain strings and scalar JSON get no badge. + */ +function redisDetectionBadgeLabel(detail: RedisMemberDetail | null): string | null { + if (!detail) return null; + if (detail.javaSerialized) return t("redis.detectedJavaSerialized"); + if (detail.pickle) return t("redis.detectedPickle"); + if (detail.msgpack) return t("redis.detectedMsgpack"); + if (detail.phpSerialized) return t("redis.detectedPhpSerialized"); + if (isRedisJsonContainerValue(detail.json?.value)) return "JSON"; + return null; +} + +const stringValueDetectionLabel = computed(() => redisDetectionBadgeLabel(stringValueDetail.value)); +const memberDetectionLabel = computed(() => redisDetectionBadgeLabel(selectedMemberDetail.value)); + +/** + * Issue #10922: payloads with a deterministic container format open decoded + * instead of as an opaque blob — but only while the user has not pinned a + * codec; any explicit codec choice (including "none") disables detection. + */ +function applyDetectedStringCodec(detail: RedisMemberDetail) { + if (isStringValueTruncated.value) return; + if (readStoredRedisValueCodec() != null) return; + stringValueCodec.value = detectedRedisStructuredCodec(detail) ?? "none"; +} + +function applyDetectedMemberCodec(detail: RedisMemberDetail) { + if (readStoredRedisValueCodec() != null) return; + memberValueCodec.value = detectedRedisStructuredCodec(detail) ?? "none"; +} + interface RedisDecodedDetail { formattedText: string; value: unknown; @@ -1275,22 +1312,27 @@ function rememberRedisJsonUnicodeMode(mode: RedisJsonUnicodeMode) { } } -function readPreferredRedisValueFormat(): RedisValueFormat { +/** + * The persisted format preference, or null when the user never pinned one. + * Null is the auto-detection signal for issue #10922: detected JSON opens in + * the JSON view instead of raw UTF-8. Legacy values ("raw") map like before. + */ +function readStoredRedisValueFormat(): RedisValueFormat | null { try { const stored = localStorage.getItem(REDIS_VALUE_FORMAT_STORAGE_KEY); if (stored === "raw") return "utf8"; - return stored === "utf8" || stored === "ascii" || stored === "binary" || stored === "json" || stored === "hex" || stored === "base64" ? stored : "utf8"; + return stored === "utf8" || stored === "ascii" || stored === "binary" || stored === "json" || stored === "hex" || stored === "base64" ? stored : null; } catch { - return "utf8"; + return null; } } -function readPreferredRedisValueCodec(): RedisValueCodec { +function readStoredRedisValueCodec(): RedisValueCodec | null { try { const stored = localStorage.getItem(REDIS_VALUE_CODEC_STORAGE_KEY); - return isRedisValueCodec(stored) ? stored : "none"; + return isRedisValueCodec(stored) ? stored : null; } catch { - return "none"; + return null; } } @@ -1634,7 +1676,13 @@ async function load(options: { background?: boolean; notifyParent?: boolean; pre const allowJsonText = loadedValue.data.kind === "string"; if (loadedValue.data.truncated) stringValueCodec.value = "none"; const detail = formatRedisMemberDetail(loadedValue.data.content, { allowJsonText }); - stringValueView.value = preferredRedisValueFormat(loadedValue.data.content, readPreferredRedisValueFormat(), { allowJsonText }); + // Issue #10922: detected JSON opens pretty-printed, detected structured + // codecs open decoded — unless the user pinned an explicit format/codec. + // Bitmap payloads render through the string pipeline but are never JSON + // or a structured codec, so detection only runs for plain strings. + stringValueView.value = autoRedisValueFormat(detail, readStoredRedisValueFormat()); + if (allowJsonText) applyDetectedStringCodec(detail); + stringJsonRawBaseline.value = detail.json?.formattedText ?? ""; stringJsonDraftBaseline.value = jsonDraftBaseline(stringJsonRawBaseline.value, redisJsonDecoded.value); editValue.value = stringValueView.value === "json" && detail.json ? stringJsonDraftBaseline.value : detail.rawText; @@ -1936,7 +1984,8 @@ function selectMember(title: string, value: unknown, context: RedisMemberContext selectedMemberKey.value = getRedisMemberSelectionKey(title, value, identity); selectedMemberContext.value = context; isEditingMember.value = false; - memberValueView.value = preferredRedisValueFormat(value, readPreferredRedisValueFormat(), { allowJsonText: true }); + memberValueView.value = autoRedisValueFormat(detail, readStoredRedisValueFormat()); + applyDetectedMemberCodec(detail); memberJsonRawBaseline.value = detail.json?.formattedText ?? ""; memberJsonDraftBaseline.value = jsonDraftBaseline(memberJsonRawBaseline.value, redisJsonDecoded.value); memberEditValue.value = memberValueView.value === "json" && detail.json ? memberJsonDraftBaseline.value : detail.rawText; @@ -2889,6 +2938,7 @@ useUpdateBlocker(() => (hasUnsavedRedisDraft.value || editingTtl.value || saving {{ redisCodecLabel(codec) }} + {{ stringValueDetectionLabel }}