mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-05 02:01:22 +02:00
feat: debug command, simplify state
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
import { useCopy } from "@/hooks/useCopy";
|
||||
import { SyntaxHighlight } from "@/components/SyntaxHighlight";
|
||||
import { CodeCopyButton } from "@/components/CodeCopyButton";
|
||||
|
||||
export function DebugViewer() {
|
||||
const { state } = useGrimoire();
|
||||
@@ -8,32 +9,26 @@ export function DebugViewer() {
|
||||
|
||||
const stateJson = JSON.stringify(state, null, 2);
|
||||
|
||||
const handleCopy = () => {
|
||||
copy(stateJson);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<h2 className="text-lg font-semibold">Application State</h2>
|
||||
<button
|
||||
onClick={() => copy(stateJson)}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 text-sm rounded-md hover:bg-muted transition-colors"
|
||||
title="Copy state to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 text-green-500" />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
<pre className="text-xs font-mono bg-muted rounded-md p-4 overflow-x-auto">
|
||||
{stateJson}
|
||||
</pre>
|
||||
<div className="flex-1 overflow-auto relative">
|
||||
<SyntaxHighlight
|
||||
code={stateJson}
|
||||
language="json"
|
||||
className="bg-muted p-4"
|
||||
/>
|
||||
<CodeCopyButton
|
||||
onCopy={handleCopy}
|
||||
copied={copied}
|
||||
label="Copy state"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
import { RelayLink } from "./nostr/RelayLink";
|
||||
import type { NostrFilter, NostrEvent } from "@/types/nostr";
|
||||
import type { NostrFilter } from "@/types/nostr";
|
||||
import {
|
||||
formatEventIds,
|
||||
formatDTags,
|
||||
@@ -678,8 +678,10 @@ export default function ReqViewer({
|
||||
// Memoize fallbackRelays to prevent re-creation on every render
|
||||
const fallbackRelays = useMemo(
|
||||
() =>
|
||||
state.activeAccount?.relays?.inbox.map((r) => r.url) || AGGREGATOR_RELAYS,
|
||||
[state.activeAccount?.relays?.inbox],
|
||||
state.activeAccount?.relays
|
||||
?.filter((r) => r.read)
|
||||
.map((r) => r.url) || AGGREGATOR_RELAYS,
|
||||
[state.activeAccount?.relays],
|
||||
);
|
||||
|
||||
// Memoize outbox options to prevent object re-creation
|
||||
@@ -749,34 +751,38 @@ export default function ReqViewer({
|
||||
// Virtuoso scroll position preservation for prepending events
|
||||
const STARTING_INDEX = 100000;
|
||||
const [firstItemIndex, setFirstItemIndex] = useState(STARTING_INDEX);
|
||||
const prevEventsRef = useRef<NostrEvent[]>([]);
|
||||
const seenEventIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
// Adjust firstItemIndex when new events are prepended to preserve scroll position
|
||||
// Uses Set-based tracking to handle rapid batches correctly
|
||||
useEffect(() => {
|
||||
const prevEvents = prevEventsRef.current;
|
||||
const currentEvents = events;
|
||||
const prevLength = prevEvents.length;
|
||||
const currentLength = currentEvents.length;
|
||||
|
||||
// Reset on clear/query change (events array shrunk)
|
||||
if (currentLength < prevLength) {
|
||||
// Reset on query change (events cleared)
|
||||
if (events.length === 0) {
|
||||
seenEventIdsRef.current = new Set();
|
||||
setFirstItemIndex(STARTING_INDEX);
|
||||
prevEventsRef.current = currentEvents;
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect new events prepended (only in streaming mode after EOSE)
|
||||
const newEventsCount = currentLength - prevLength;
|
||||
if (newEventsCount > 0 && prevLength > 0 && stream && eoseReceived) {
|
||||
// Verify first event changed (events were prepended, not inserted in middle)
|
||||
const firstIdChanged = currentEvents[0]?.id !== prevEvents[0]?.id;
|
||||
if (firstIdChanged) {
|
||||
// Decrement firstItemIndex to maintain scroll position
|
||||
setFirstItemIndex((prev) => prev - newEventsCount);
|
||||
// Find new events at the start of the array (prepended)
|
||||
// This approach is immune to rapid updates because we track ALL seen IDs cumulatively
|
||||
let prependCount = 0;
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
if (!seenEventIdsRef.current.has(event.id)) {
|
||||
// New event found at position i
|
||||
prependCount++;
|
||||
seenEventIdsRef.current.add(event.id);
|
||||
} else {
|
||||
// Found first existing event, stop counting
|
||||
// All events after this are old (already seen)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prevEventsRef.current = currentEvents;
|
||||
// Adjust index only in streaming mode after EOSE
|
||||
if (prependCount > 0 && stream && eoseReceived) {
|
||||
setFirstItemIndex((prev) => prev - prependCount);
|
||||
}
|
||||
}, [events, stream, eoseReceived]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,14 +107,14 @@ export default function UserMenu() {
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
{relays && relays.all.length > 0 && (
|
||||
{relays && relays.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground font-normal">
|
||||
Relays
|
||||
</DropdownMenuLabel>
|
||||
{relays.all.map((relay) => (
|
||||
{relays.map((relay) => (
|
||||
<RelayLink
|
||||
className="px-2 py-1"
|
||||
urlClassname="text-sm"
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MosaicNode } from "react-mosaic-component";
|
||||
import {
|
||||
GrimoireState,
|
||||
WindowInstance,
|
||||
UserRelays,
|
||||
RelayInfo,
|
||||
LayoutConfig,
|
||||
} from "@/types/app";
|
||||
import { insertWindow } from "@/lib/layout-utils";
|
||||
@@ -273,7 +273,7 @@ export const setActiveAccount = (
|
||||
*/
|
||||
export const setActiveAccountRelays = (
|
||||
state: GrimoireState,
|
||||
relays: UserRelays,
|
||||
relays: RelayInfo[],
|
||||
): GrimoireState => {
|
||||
if (!state.activeAccount) {
|
||||
return state;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AppId,
|
||||
WindowInstance,
|
||||
LayoutConfig,
|
||||
RelayInfo,
|
||||
} from "@/types/app";
|
||||
import { useLocale } from "@/hooks/useLocale";
|
||||
import * as Logic from "./logic";
|
||||
@@ -255,7 +256,7 @@ export const useGrimoire = () => {
|
||||
);
|
||||
|
||||
const setActiveAccountRelays = useCallback(
|
||||
(relays: any) =>
|
||||
(relays: RelayInfo[]) =>
|
||||
setState((prev) => Logic.setActiveAccountRelays(prev, relays)),
|
||||
[setState],
|
||||
);
|
||||
|
||||
@@ -2,9 +2,8 @@ import { useEffect } from "react";
|
||||
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
|
||||
import accounts from "@/services/accounts";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
|
||||
import { addressLoader } from "@/services/loaders";
|
||||
import type { RelayInfo, UserRelays } from "@/types/app";
|
||||
import type { RelayInfo } from "@/types/app";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
|
||||
/**
|
||||
@@ -48,12 +47,10 @@ export function useAccountSync() {
|
||||
if (relayListEvent.id === lastRelayEventId) return;
|
||||
lastRelayEventId = relayListEvent.id;
|
||||
|
||||
// Parse inbox and outbox relays
|
||||
const inboxRelays = getInboxes(relayListEvent);
|
||||
const outboxRelays = getOutboxes(relayListEvent);
|
||||
|
||||
// Get all relays from tags
|
||||
const allRelays: RelayInfo[] = [];
|
||||
// Parse relays from tags (NIP-65 format)
|
||||
// Tag format: ["r", "relay-url", "read|write"]
|
||||
// If no marker, relay is used for both read and write
|
||||
const relays: RelayInfo[] = [];
|
||||
const seenUrls = new Set<string>();
|
||||
|
||||
for (const tag of relayListEvent.tags) {
|
||||
@@ -63,11 +60,11 @@ export function useAccountSync() {
|
||||
if (seenUrls.has(url)) continue;
|
||||
seenUrls.add(url);
|
||||
|
||||
const type = tag[2];
|
||||
allRelays.push({
|
||||
const marker = tag[2];
|
||||
relays.push({
|
||||
url,
|
||||
read: !type || type === "read",
|
||||
write: !type || type === "write",
|
||||
read: !marker || marker === "read",
|
||||
write: !marker || marker === "write",
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
@@ -78,36 +75,6 @@ export function useAccountSync() {
|
||||
}
|
||||
}
|
||||
|
||||
const relays: UserRelays = {
|
||||
inbox: inboxRelays
|
||||
.map((url) => {
|
||||
try {
|
||||
return {
|
||||
url: normalizeRelayURL(url),
|
||||
read: true,
|
||||
write: false,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((r): r is RelayInfo => r !== null),
|
||||
outbox: outboxRelays
|
||||
.map((url) => {
|
||||
try {
|
||||
return {
|
||||
url: normalizeRelayURL(url),
|
||||
read: false,
|
||||
write: true,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((r): r is RelayInfo => r !== null),
|
||||
all: allRelays,
|
||||
};
|
||||
|
||||
setActiveAccountRelays(relays);
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { GrimoireState } from "@/types/app";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const CURRENT_VERSION = 8;
|
||||
export const CURRENT_VERSION = 9;
|
||||
|
||||
/**
|
||||
* Migration function type
|
||||
@@ -81,6 +81,30 @@ const migrations: Record<number, MigrationFn> = {
|
||||
},
|
||||
};
|
||||
},
|
||||
// Migration from v8 to v9 - simplifies relay structure
|
||||
8: (state: any) => {
|
||||
// Simplify activeAccount.relays from {inbox, outbox, all} to just an array
|
||||
// The 'all' array already has the correct read/write flags per relay
|
||||
if (state.activeAccount?.relays) {
|
||||
const oldRelays = state.activeAccount.relays;
|
||||
// If it has the old structure (with inbox/outbox/all), migrate it
|
||||
if (oldRelays.all && Array.isArray(oldRelays.all)) {
|
||||
return {
|
||||
...state,
|
||||
__version: 9,
|
||||
activeAccount: {
|
||||
...state.activeAccount,
|
||||
relays: oldRelays.all,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
// No relays to migrate, just bump version
|
||||
return {
|
||||
...state,
|
||||
__version: 9,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,12 +72,6 @@ export interface RelayInfo {
|
||||
write: boolean;
|
||||
}
|
||||
|
||||
export interface UserRelays {
|
||||
inbox: RelayInfo[];
|
||||
outbox: RelayInfo[];
|
||||
all: RelayInfo[];
|
||||
}
|
||||
|
||||
export interface GrimoireState {
|
||||
__version: number; // Schema version for migrations
|
||||
windows: Record<string, WindowInstance>;
|
||||
@@ -86,7 +80,7 @@ export interface GrimoireState {
|
||||
layoutConfig: LayoutConfig; // Global configuration for window insertion behavior
|
||||
activeAccount?: {
|
||||
pubkey: string;
|
||||
relays?: UserRelays;
|
||||
relays?: RelayInfo[];
|
||||
};
|
||||
locale?: {
|
||||
locale: string;
|
||||
|
||||
@@ -108,18 +108,18 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
category: "Documentation",
|
||||
defaultProps: {},
|
||||
},
|
||||
// debug: {
|
||||
// name: "debug",
|
||||
// section: "1",
|
||||
// synopsis: "debug",
|
||||
// description:
|
||||
// "Display the current application state for debugging purposes. Shows windows, workspaces, active account, and other internal state in a formatted view.",
|
||||
// examples: ["debug View current application state"],
|
||||
// seeAlso: ["help"],
|
||||
// appId: "debug",
|
||||
// category: "System",
|
||||
// defaultProps: {},
|
||||
// },
|
||||
debug: {
|
||||
name: "debug",
|
||||
section: "1",
|
||||
synopsis: "debug",
|
||||
description:
|
||||
"Display the current application state for debugging purposes. Shows windows, workspaces, active account, and other internal state in a formatted view.",
|
||||
examples: ["debug View current application state"],
|
||||
seeAlso: ["help"],
|
||||
appId: "debug",
|
||||
category: "System",
|
||||
defaultProps: {},
|
||||
},
|
||||
man: {
|
||||
name: "man",
|
||||
section: "1",
|
||||
|
||||
Reference in New Issue
Block a user