feat(desktop): clarify inspector readability path
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -57,6 +57,10 @@ import {
|
||||
buildStructuredInspectorSections,
|
||||
type StructuredInspectorSection,
|
||||
} from "./lib/structured-inspector";
|
||||
import {
|
||||
buildInspectorReadabilitySummary,
|
||||
type InspectorFact,
|
||||
} from "./lib/inspector-readability";
|
||||
import {
|
||||
appendWorkspaceKeyRecords,
|
||||
filterWorkspaceKeys,
|
||||
@@ -475,14 +479,25 @@ function App() {
|
||||
!isInspectorLoading &&
|
||||
structuredInspectorSections.length > 0 &&
|
||||
!isEditable;
|
||||
|
||||
const supportNote = tauriAvailable
|
||||
? buildLiveSupportNote(selectedValueRecord, selectedKey)
|
||||
: (selectedKey?.summary ??
|
||||
`Search currently returns zero keys in ${activeDb}. This is an intentional empty state, not a backend failure.`);
|
||||
const ttlWriteSupported = tauriAvailable
|
||||
? selectedValueRecord?.ttl_write_supported === true
|
||||
: selectedKey !== null;
|
||||
const inspectorReadability = buildInspectorReadabilitySummary({
|
||||
databaseLabel: activeDb,
|
||||
runtimeMode: tauriAvailable ? "live" : "demo",
|
||||
selectedKey:
|
||||
selectedKey === null
|
||||
? null
|
||||
: {
|
||||
name: selectedKey.name,
|
||||
type: selectedKey.type,
|
||||
ttl: selectedKey.ttl,
|
||||
},
|
||||
record: selectedValueRecord,
|
||||
isLoading: isInspectorLoading,
|
||||
isEditable,
|
||||
ttlWriteSupported,
|
||||
});
|
||||
const localSmokeFixtureActive = isLocalSmokeFixtureConnection(activeConnection);
|
||||
const localSmokeFilterActive = search.trim() === localSmokeFixtureSearchText;
|
||||
const ttlDraftError = ttlDraft.trim() ? validateTtlDraft(ttlDraft) : null;
|
||||
@@ -1843,6 +1858,7 @@ function App() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="neutral">{selectedKey.type}</Badge>
|
||||
<Badge variant="neutral">TTL {selectedKey.ttl}</Badge>
|
||||
<Badge variant="accent">{inspectorReadability.status}</Badge>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1852,16 +1868,19 @@ function App() {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">Value surface</p>
|
||||
<span className="font-mono text-xs text-[var(--ink-muted)]">
|
||||
{selectedKey
|
||||
? isInspectorLoading
|
||||
? "loading"
|
||||
: isEditable
|
||||
? "editable"
|
||||
: "read only"
|
||||
: "awaiting selection"}
|
||||
{inspectorReadability.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-[20px] border border-[var(--line-soft)] bg-[var(--surface-muted)] px-4 py-3">
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">
|
||||
{inspectorReadability.surfaceTitle}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-soft)]">
|
||||
{inspectorReadability.surfaceDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedKey ? (
|
||||
showStructuredInspector ? (
|
||||
<StructuredInspectorPanels sections={structuredInspectorSections} />
|
||||
@@ -1939,17 +1958,29 @@ function App() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-[20px] border border-dashed border-[var(--line-soft)] bg-[var(--surface-muted)] px-4 py-3">
|
||||
<p className="font-mono text-[11px] uppercase tracking-[0.18em] text-[var(--ink-muted)]">
|
||||
{inspectorReadability.actionTitle}
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[var(--ink-soft)]">
|
||||
{inspectorReadability.actionDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<InfoCard
|
||||
title="Key metadata"
|
||||
<InspectorSummaryCard
|
||||
title="Inspect summary"
|
||||
icon={<CircleAlert className="h-4 w-4" />}
|
||||
surfaceDescription={inspectorReadability.surfaceDescription}
|
||||
actionTitle={inspectorReadability.actionTitle}
|
||||
actionDescription={inspectorReadability.actionDescription}
|
||||
/>
|
||||
<InspectorFactsCard
|
||||
title="Key facts"
|
||||
icon={<TimerReset className="h-4 w-4" />}
|
||||
lines={[
|
||||
`Active DB: ${activeDb}`,
|
||||
`TTL: ${selectedKey?.ttl ?? "no active key"}`,
|
||||
`Editor support: ${selectedKey ? (isEditable ? "string write enabled" : "inspection only") : "awaiting matching selection"}`,
|
||||
]}
|
||||
facts={inspectorReadability.facts}
|
||||
/>
|
||||
<div className="rounded-[24px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -2014,11 +2045,6 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfoCard
|
||||
title="Support note"
|
||||
icon={<CircleAlert className="h-4 w-4" />}
|
||||
lines={[supportNote]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2150,6 +2176,59 @@ function InfoCard({
|
||||
);
|
||||
}
|
||||
|
||||
function InspectorSummaryCard({
|
||||
title,
|
||||
icon,
|
||||
surfaceDescription,
|
||||
actionTitle,
|
||||
actionDescription,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactElement;
|
||||
surfaceDescription: string;
|
||||
actionTitle: string;
|
||||
actionDescription: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-[24px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--accent-strong)]">{icon}</span>
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">{title}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-4">
|
||||
<InfoLine label="Current path" value={surfaceDescription} />
|
||||
<InfoLine label={actionTitle} value={actionDescription} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InspectorFactsCard({
|
||||
title,
|
||||
icon,
|
||||
facts,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactElement;
|
||||
facts: InspectorFact[];
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-[24px] border border-[var(--line-soft)] bg-[var(--surface-base)] p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[var(--accent-strong)]">{icon}</span>
|
||||
<p className="text-sm font-medium text-[var(--ink-0)]">{title}</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2">
|
||||
{facts.map((fact) => (
|
||||
<InfoLine key={fact.label} label={fact.label} value={fact.value} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StructuredInspectorPanels({
|
||||
sections,
|
||||
}: {
|
||||
|
||||
183
apps/desktop/src/lib/inspector-readability.test.ts
Normal file
183
apps/desktop/src/lib/inspector-readability.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildInspectorReadabilitySummary } from "./inspector-readability";
|
||||
|
||||
describe("inspector readability summary", () => {
|
||||
it("guides the operator when no key is selected", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db4",
|
||||
runtimeMode: "live",
|
||||
selectedKey: null,
|
||||
record: null,
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: false,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "awaiting selection",
|
||||
surfaceTitle: "No key selected",
|
||||
actionTitle: "Next step",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Scope", value: "db4" },
|
||||
{ label: "Type", value: "unknown" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("describes editable live strings with explicit write semantics", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "cache:homepage",
|
||||
type: "string",
|
||||
ttl: "48s",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "string",
|
||||
ttl: { kind: "expires_in_millis", value: 48_000 },
|
||||
},
|
||||
data: {
|
||||
kind: "string",
|
||||
value: { kind: "string", value: "payload" },
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: true,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual({
|
||||
status: "editable string",
|
||||
surfaceTitle: "String value",
|
||||
surfaceDescription:
|
||||
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
|
||||
facts: [
|
||||
{ label: "Scope", value: "db0" },
|
||||
{ label: "Type", value: "string" },
|
||||
{ label: "TTL state", value: "48s" },
|
||||
{ label: "Value shape", value: "string payload" },
|
||||
{ label: "Write path", value: "replace full string" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("describes structured live values as readable inspection-only surfaces", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "session:42",
|
||||
type: "hash",
|
||||
ttl: "13m",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "hash",
|
||||
ttl: { kind: "expires_in_millis", value: 780_000 },
|
||||
},
|
||||
data: {
|
||||
kind: "hash",
|
||||
entries: [
|
||||
{
|
||||
field: { kind: "string", value: "region" },
|
||||
value: { kind: "string", value: "eu-west-1" },
|
||||
},
|
||||
{
|
||||
field: { kind: "string", value: "role" },
|
||||
value: { kind: "string", value: "editor" },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "structured read only",
|
||||
surfaceTitle: "Hash fields",
|
||||
facts: [
|
||||
{ label: "Scope", value: "db0" },
|
||||
{ label: "Type", value: "hash" },
|
||||
{ label: "TTL state", value: "13m" },
|
||||
{ label: "Value shape", value: "2 fields" },
|
||||
{ label: "Write path", value: "inspection only" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps demo mode explicit so shell review is not confused with live evidence", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db0",
|
||||
runtimeMode: "demo",
|
||||
selectedKey: {
|
||||
name: "cache:homepage",
|
||||
type: "string",
|
||||
ttl: "48s",
|
||||
},
|
||||
record: null,
|
||||
isLoading: false,
|
||||
isEditable: true,
|
||||
ttlWriteSupported: true,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "demo editable",
|
||||
surfaceTitle: "String value",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Write path", value: "session-local string save" },
|
||||
{ label: "TTL control", value: "available" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("makes stale browse-to-read gaps explicit when the key disappears", () => {
|
||||
expect(
|
||||
buildInspectorReadabilitySummary({
|
||||
databaseLabel: "db2",
|
||||
runtimeMode: "live",
|
||||
selectedKey: {
|
||||
name: "jobs:failed",
|
||||
type: "list",
|
||||
ttl: "persistent",
|
||||
},
|
||||
record: {
|
||||
metadata: {
|
||||
key_type: "list",
|
||||
ttl: { kind: "missing" },
|
||||
},
|
||||
data: {
|
||||
kind: "missing",
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isEditable: false,
|
||||
ttlWriteSupported: false,
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
status: "missing key",
|
||||
surfaceTitle: "Key disappeared after browse",
|
||||
facts: expect.arrayContaining([
|
||||
{ label: "Scope", value: "db2" },
|
||||
{ label: "TTL state", value: "missing" },
|
||||
{ label: "Write path", value: "not available" },
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
340
apps/desktop/src/lib/inspector-readability.ts
Normal file
340
apps/desktop/src/lib/inspector-readability.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
type RedisKeyTtl =
|
||||
| { kind: "persistent" }
|
||||
| { kind: "missing" }
|
||||
| { kind: "expires_in_millis"; value: number };
|
||||
|
||||
type RedisValueContent =
|
||||
| { kind: "string"; value: string }
|
||||
| { kind: "binary"; bytes: number[] };
|
||||
|
||||
type RedisFieldValueEntry = {
|
||||
field: RedisValueContent;
|
||||
value: RedisValueContent;
|
||||
};
|
||||
|
||||
type RedisSortedSetEntry = {
|
||||
member: RedisValueContent;
|
||||
score: string;
|
||||
};
|
||||
|
||||
type RedisStreamEntry = {
|
||||
id: string;
|
||||
fields: RedisFieldValueEntry[];
|
||||
};
|
||||
|
||||
type RedisValueData =
|
||||
| { kind: "missing" }
|
||||
| { kind: "string"; value: RedisValueContent }
|
||||
| { kind: "hash"; entries: RedisFieldValueEntry[] }
|
||||
| { kind: "list"; items: RedisValueContent[] }
|
||||
| { kind: "set"; members: RedisValueContent[] }
|
||||
| { kind: "sorted_set"; entries: RedisSortedSetEntry[] }
|
||||
| { kind: "stream"; entries: RedisStreamEntry[] };
|
||||
|
||||
type RedisValueRecord = {
|
||||
metadata: {
|
||||
key_type: string;
|
||||
ttl: RedisKeyTtl;
|
||||
};
|
||||
data: RedisValueData;
|
||||
};
|
||||
|
||||
type SelectedInspectorKey = {
|
||||
name: string;
|
||||
type: string;
|
||||
ttl: string;
|
||||
};
|
||||
|
||||
export type InspectorFact = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type InspectorReadabilitySummary = {
|
||||
status: string;
|
||||
surfaceTitle: string;
|
||||
surfaceDescription: string;
|
||||
actionTitle: string;
|
||||
actionDescription: string;
|
||||
facts: InspectorFact[];
|
||||
};
|
||||
|
||||
export type InspectorReadabilityContext = {
|
||||
databaseLabel: string;
|
||||
runtimeMode: "live" | "demo";
|
||||
selectedKey: SelectedInspectorKey | null;
|
||||
record: RedisValueRecord | null;
|
||||
isLoading: boolean;
|
||||
isEditable: boolean;
|
||||
ttlWriteSupported: boolean;
|
||||
};
|
||||
|
||||
export function buildInspectorReadabilitySummary(
|
||||
context: InspectorReadabilityContext,
|
||||
): InspectorReadabilitySummary {
|
||||
const keyType = resolveKeyType(context);
|
||||
const ttlState = resolveTtlState(context);
|
||||
const valueShape = describeValueShape(context);
|
||||
const writePath = describeWritePath(context);
|
||||
const ttlPath = context.selectedKey
|
||||
? context.ttlWriteSupported
|
||||
? "available"
|
||||
: "not available"
|
||||
: "awaiting selection";
|
||||
const facts: InspectorFact[] = [
|
||||
{ label: "Scope", value: context.databaseLabel },
|
||||
{ label: "Type", value: formatRedisKeyTypeLabel(keyType) },
|
||||
{ label: "TTL state", value: ttlState },
|
||||
{ label: "Value shape", value: valueShape },
|
||||
{ label: "Write path", value: writePath },
|
||||
{ label: "TTL control", value: ttlPath },
|
||||
];
|
||||
|
||||
if (!context.selectedKey) {
|
||||
return {
|
||||
status: "awaiting selection",
|
||||
surfaceTitle: "No key selected",
|
||||
surfaceDescription:
|
||||
"Pick a key from the browser to load its metadata, value shape, and TTL state into the inspector.",
|
||||
actionTitle: "Next step",
|
||||
actionDescription:
|
||||
"If the browser is empty, clear or adjust the current search so the inspector has a live target again.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return {
|
||||
status: context.isEditable ? "demo editable" : "demo read only",
|
||||
surfaceTitle: buildSurfaceTitle(context.selectedKey.type, false),
|
||||
surfaceDescription: context.isEditable
|
||||
? "Browser dev mode is showing a session-local string payload. Layout review is safe here, but no live Redis mutation occurs."
|
||||
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} payloads stay on the demo inspection path in browser mode so the UI can be reviewed without pretending a live backend exists.`,
|
||||
actionTitle: "Operator path",
|
||||
actionDescription: context.isEditable
|
||||
? "Use Save string value only for session-local shell review. Switch to the desktop runtime before treating the result as Redis evidence."
|
||||
: "Review the current structure here, then use the desktop runtime when you need live Redis data or verification.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return {
|
||||
status: "loading",
|
||||
surfaceTitle: `Loading ${formatRedisKeyTypeLabel(keyType)} value`,
|
||||
surfaceDescription:
|
||||
"The desktop bridge is reading the current key so the inspector can show live metadata, value shape, and write capability together.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Wait for the read to finish before editing or relying on the TTL state shown in the inspector.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return {
|
||||
status: "metadata only",
|
||||
surfaceTitle: `${formatRedisKeyTypeLabel(keyType)} metadata ready`,
|
||||
surfaceDescription:
|
||||
"The browser still has metadata for the selected key, but the live value surface is not currently loaded in the inspector.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Use Refresh metadata to retry the live read, or return to the browser if the key may have changed upstream.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.record.data.kind === "missing") {
|
||||
return {
|
||||
status: "missing key",
|
||||
surfaceTitle: "Key disappeared after browse",
|
||||
surfaceDescription:
|
||||
"The key was visible during browse but no longer existed when the inspector requested the live value.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Refresh metadata or reload the browser list to clear the stale selection before taking further action.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
if (context.isEditable) {
|
||||
return {
|
||||
status: "editable string",
|
||||
surfaceTitle: "String value",
|
||||
surfaceDescription:
|
||||
"The full live string payload is loaded in the inspector. Saving replaces the entire string value and preserves the active TTL boundary.",
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Review the full payload, make bounded edits, and use Save string value when you are ready to replace the current value.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "structured read only",
|
||||
surfaceTitle: buildSurfaceTitle(context.record.data.kind, true),
|
||||
surfaceDescription: buildStructuredDescription(context.record.data.kind),
|
||||
actionTitle: "Operator path",
|
||||
actionDescription:
|
||||
"Use the structured panels to scan the live payload quickly. Mutations for this key type remain outside the current desktop contract.",
|
||||
facts,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveKeyType(context: InspectorReadabilityContext) {
|
||||
return context.record?.metadata.key_type ?? context.selectedKey?.type ?? "unknown";
|
||||
}
|
||||
|
||||
function resolveTtlState(context: InspectorReadabilityContext) {
|
||||
if (context.record) {
|
||||
return formatRedisKeyTtl(context.record.metadata.ttl);
|
||||
}
|
||||
|
||||
return context.selectedKey?.ttl ?? "awaiting selection";
|
||||
}
|
||||
|
||||
function describeValueShape(context: InspectorReadabilityContext) {
|
||||
if (!context.selectedKey) {
|
||||
return "no active key";
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return context.isEditable
|
||||
? "session-local string payload"
|
||||
: `${formatRedisKeyTypeLabel(context.selectedKey.type)} demo preview`;
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return "live value request in flight";
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return "metadata loaded, value unavailable";
|
||||
}
|
||||
|
||||
switch (context.record.data.kind) {
|
||||
case "missing":
|
||||
return "key missing";
|
||||
case "string":
|
||||
return "string payload";
|
||||
case "hash":
|
||||
return `${context.record.data.entries.length} field${context.record.data.entries.length === 1 ? "" : "s"}`;
|
||||
case "list":
|
||||
return `${context.record.data.items.length} item${context.record.data.items.length === 1 ? "" : "s"}`;
|
||||
case "set":
|
||||
return `${context.record.data.members.length} member${context.record.data.members.length === 1 ? "" : "s"}`;
|
||||
case "sorted_set":
|
||||
return `${context.record.data.entries.length} scored member${context.record.data.entries.length === 1 ? "" : "s"}`;
|
||||
case "stream":
|
||||
return `${context.record.data.entries.length} entr${context.record.data.entries.length === 1 ? "y" : "ies"}`;
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function describeWritePath(context: InspectorReadabilityContext) {
|
||||
if (!context.selectedKey) {
|
||||
return "awaiting selection";
|
||||
}
|
||||
|
||||
if (context.runtimeMode === "demo") {
|
||||
return context.isEditable ? "session-local string save" : "inspection only";
|
||||
}
|
||||
|
||||
if (context.isLoading) {
|
||||
return "pending live capability";
|
||||
}
|
||||
|
||||
if (!context.record) {
|
||||
return "value unavailable";
|
||||
}
|
||||
|
||||
if (context.record.data.kind === "missing") {
|
||||
return "not available";
|
||||
}
|
||||
|
||||
return context.isEditable ? "replace full string" : "inspection only";
|
||||
}
|
||||
|
||||
function buildSurfaceTitle(kind: string, liveStructured: boolean) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Hash fields";
|
||||
case "list":
|
||||
return "List items";
|
||||
case "set":
|
||||
return liveStructured ? "Set members" : "Set preview";
|
||||
case "sorted_set":
|
||||
case "zset":
|
||||
return "Sorted set members";
|
||||
case "stream":
|
||||
return "Stream entries";
|
||||
case "string":
|
||||
return "String value";
|
||||
default:
|
||||
return "Value surface";
|
||||
}
|
||||
}
|
||||
|
||||
function buildStructuredDescription(kind: string) {
|
||||
switch (kind) {
|
||||
case "hash":
|
||||
return "Live HGETALL output is rendered as field/value rows so operators do not need to parse raw JSON during inspection.";
|
||||
case "list":
|
||||
return "Live LRANGE output is rendered in list order so operators can scan indexed items without leaving the inspector.";
|
||||
case "set":
|
||||
return "Live SMEMBERS output is rendered as a structured member list so the inspector stays readable even when the payload is dense.";
|
||||
case "sorted_set":
|
||||
return "Live ZRANGE output is rendered as member and score pairs so ordering semantics stay visible in the inspector.";
|
||||
case "stream":
|
||||
return "Live XRANGE output is rendered as entry rows with inline field payloads so stream structure is visible at a glance.";
|
||||
default:
|
||||
return "The live value is available on a structured read-only path in the current desktop contract.";
|
||||
}
|
||||
}
|
||||
|
||||
function formatRedisKeyTtl(ttl: RedisKeyTtl) {
|
||||
switch (ttl.kind) {
|
||||
case "persistent":
|
||||
return "persistent";
|
||||
case "missing":
|
||||
return "missing";
|
||||
case "expires_in_millis":
|
||||
return formatDurationFromMillis(ttl.value);
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
function formatDurationFromMillis(value: number) {
|
||||
if (value < 1_000) {
|
||||
return `${value}ms`;
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(value / 1_000);
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds}s`;
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h`;
|
||||
}
|
||||
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
return `${totalDays}d`;
|
||||
}
|
||||
|
||||
function formatRedisKeyTypeLabel(value: string) {
|
||||
switch (value) {
|
||||
case "zset":
|
||||
return "sorted set";
|
||||
default:
|
||||
return value || "unknown";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user