Add follow mode (-f) to REQ command (#220)

* feat: add -f (follow) option to req command

Add tail -f style auto-refresh behavior to the req command. When enabled,
new events are automatically displayed instead of being buffered behind
a "X new events" button.
This commit is contained in:
Alejandro
2026-01-27 13:47:19 +01:00
committed by GitHub
parent d69cc1fec6
commit 3282581636
5 changed files with 51 additions and 4 deletions

View File

@@ -117,6 +117,7 @@ interface ReqViewerProps {
relays?: string[];
closeOnEose?: boolean;
view?: ViewMode;
follow?: boolean; // Auto-refresh mode (like tail -f)
nip05Authors?: string[];
nip05PTags?: string[];
domainAuthors?: string[];
@@ -713,6 +714,7 @@ export default function ReqViewer({
relays,
closeOnEose = false,
view = "list",
follow = false,
nip05Authors,
nip05PTags,
domainAuthors,
@@ -846,8 +848,11 @@ export default function ReqViewer({
const [isFrozen, setIsFrozen] = useState(false);
const virtuosoRef = useRef<any>(null);
// Freeze timeline after EOSE in streaming mode
// Freeze timeline after EOSE in streaming mode (skip if follow mode enabled)
useEffect(() => {
// Don't freeze in follow mode - show events as they arrive
if (follow) return;
// Freeze after EOSE in streaming mode
if (eoseReceived && stream && !isFrozen && events.length > 0) {
setFreezePoint(events[0].id);
@@ -859,7 +864,7 @@ export default function ReqViewer({
setFreezePoint(null);
setIsFrozen(false);
}
}, [eoseReceived, stream, isFrozen, events]);
}, [follow, eoseReceived, stream, isFrozen, events]);
// Filter events based on freeze point
const { visibleEvents, newEventCount } = useMemo(() => {
@@ -1343,8 +1348,8 @@ export default function ReqViewer({
{/* Results */}
{(!needsAccount || accountPubkey) && (
<div className="flex-1 overflow-y-auto relative">
{/* Floating "New Events" Button */}
{isFrozen && newEventCount > 0 && (
{/* Floating "New Events" Button (hidden in follow mode) */}
{isFrozen && newEventCount > 0 && !follow && (
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10">
<Button
onClick={handleUnfreeze}

View File

@@ -162,6 +162,7 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) {
relays={window.props.relays}
closeOnEose={window.props.closeOnEose}
view={window.props.view}
follow={window.props.follow}
nip05Authors={window.props.nip05Authors}
nip05PTags={window.props.nip05PTags}
domainAuthors={window.props.domainAuthors}

View File

@@ -1402,6 +1402,30 @@ describe("parseReqCommand", () => {
});
});
describe("follow flag (-f, --follow)", () => {
it("should parse -f flag", () => {
const result = parseReqCommand(["-f"]);
expect(result.follow).toBe(true);
});
it("should parse --follow flag", () => {
const result = parseReqCommand(["--follow"]);
expect(result.follow).toBe(true);
});
it("should default to false when not provided", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.follow).toBe(false);
});
it("should work with other flags", () => {
const result = parseReqCommand(["-k", "1", "-f", "-l", "50"]);
expect(result.filter.kinds).toEqual([1]);
expect(result.follow).toBe(true);
expect(result.filter.limit).toBe(50);
});
});
describe("complex scenarios", () => {
it("should handle multiple flags together", () => {
const hex = "a".repeat(64);

View File

@@ -15,6 +15,7 @@ export interface ParsedReqCommand {
relays?: string[];
closeOnEose?: boolean;
view?: ViewMode; // Display mode for results
follow?: boolean; // Auto-refresh mode (like tail -f)
nip05Authors?: string[]; // NIP-05 identifiers that need async resolution
nip05PTags?: string[]; // NIP-05 identifiers for #p tags that need async resolution
nip05PTagsUppercase?: string[]; // NIP-05 identifiers for #P tags that need async resolution
@@ -83,6 +84,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
let closeOnEose = false;
let view: ViewMode | undefined;
let follow = false;
let i = 0;
@@ -414,6 +416,13 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
break;
}
case "-f":
case "--follow": {
follow = true;
i++;
break;
}
case "-T":
case "--tag": {
// Generic tag filter: --tag <letter> <value>
@@ -490,6 +499,7 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
relays: relays.length > 0 ? relays : undefined,
closeOnEose,
view,
follow,
nip05Authors: nip05Authors.size > 0 ? Array.from(nip05Authors) : undefined,
nip05PTags: nip05PTags.size > 0 ? Array.from(nip05PTags) : undefined,
nip05PTagsUppercase:

View File

@@ -234,6 +234,11 @@ export const manPages: Record<string, ManPageEntry> = {
description:
"Display mode for results. 'list' shows full event cards, 'compact' shows condensed single-line rows. Defaults to 'list'.",
},
{
flag: "-f, --follow",
description:
"Auto-refresh mode (like tail -f). Automatically displays new events instead of buffering them behind a 'X new events' button. Refreshes every second.",
},
{
flag: "[relay...]",
description:
@@ -272,6 +277,8 @@ export const manPages: Record<string, ManPageEntry> = {
"req --search bitcoin -k 1 Search notes for 'bitcoin'",
"req -k 1 theforest.nostr1.com relay.damus.io Query specific relays (overrides auto-selection)",
"req -k 1 -l 100 --view compact Get notes in compact view mode",
"req -k 1 -f Follow mode: auto-display new notes (1s refresh)",
"req -k 1 -a $contacts -f Follow your contacts' notes in real-time",
],
seeAlso: ["kind", "nip"],
appId: "req",