diff --git a/src/commands/benchmark-job/run.ts b/src/commands/benchmark-job/run.ts index 9e5b0e9..a22ae53 100644 --- a/src/commands/benchmark-job/run.ts +++ b/src/commands/benchmark-job/run.ts @@ -204,16 +204,31 @@ async function ensureAgentSecrets( return secrets; } +const BENCHMARK_ID_PREFIXES = ["bm_", "bmk_", "bmd_"]; + +function looksLikeBenchmarkId(s: string): boolean { + return BENCHMARK_ID_PREFIXES.some((p) => s.startsWith(p)); +} + +// Extract a benchmark ID from strings like "Name (bmd_xxx)" copied from the TUI +function extractEmbeddedId(s: string): string | null { + const match = s.match(/\((bm[dk]?_\S+)\)\s*$/); + return match ? match[1] : null; +} + // Resolve benchmark name to ID if needed async function resolveBenchmarkId(benchmarkIdOrName: string): Promise { - // If it looks like an ID (starts with bm_ or similar), return as-is - if ( - benchmarkIdOrName.startsWith("bm_") || - benchmarkIdOrName.startsWith("bmk_") - ) { + // If it looks like a bare ID, return as-is + if (looksLikeBenchmarkId(benchmarkIdOrName)) { return benchmarkIdOrName; } + // If the input has an embedded ID like "Name (bmd_xxx)", extract and use it + const embeddedId = extractEmbeddedId(benchmarkIdOrName); + if (embeddedId) { + return embeddedId; + } + // Search both user benchmarks and public benchmarks const [userResult, publicResult] = await Promise.all([ listBenchmarks({ diff --git a/src/screens/BenchmarkDetailScreen.tsx b/src/screens/BenchmarkDetailScreen.tsx index 4bc7b33..9d518b4 100644 --- a/src/screens/BenchmarkDetailScreen.tsx +++ b/src/screens/BenchmarkDetailScreen.tsx @@ -176,10 +176,10 @@ export function BenchmarkDetailScreen({ const operations: ResourceOperation[] = [ { key: "create-job", - label: "Create Benchmark Job", + label: "Run Benchmark Job", color: colors.success, icon: figures.play, - shortcut: "c", + shortcut: "r", }, ]; diff --git a/src/screens/BenchmarkJobListScreen.tsx b/src/screens/BenchmarkJobListScreen.tsx index 83475e0..108476c 100644 --- a/src/screens/BenchmarkJobListScreen.tsx +++ b/src/screens/BenchmarkJobListScreen.tsx @@ -598,7 +598,7 @@ export function BenchmarkJobListScreen() { data={benchmarkJobs} keyExtractor={(job: BenchmarkJob) => job.id} selectedIndex={selectedIndex} - title={`benchmark_jobs[${totalCount}]`} + title={`benchmark_jobs[${totalCount}${hasMore ? "+" : ""}]`} columns={columns} emptyState={ @@ -613,12 +613,13 @@ export function BenchmarkJobListScreen() { {figures.hamburger} {totalCount} + {hasMore ? "+" : ""} {" "} total - {totalPages > 1 && ( + {(hasMore || hasPrev) && ( <> {" "} @@ -630,7 +631,8 @@ export function BenchmarkJobListScreen() { ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} + {!hasMore ? ` of ${totalPages}` : ""} )} @@ -640,7 +642,8 @@ export function BenchmarkJobListScreen() { •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} + {!hasMore ? ` of ${totalCount}` : ""} )} diff --git a/src/screens/BenchmarkListScreen.tsx b/src/screens/BenchmarkListScreen.tsx index fe953b3..7e753e6 100644 --- a/src/screens/BenchmarkListScreen.tsx +++ b/src/screens/BenchmarkListScreen.tsx @@ -24,15 +24,32 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js"; import { useCursorPagination } from "../hooks/useCursorPagination.js"; import { useListSearch } from "../hooks/useListSearch.js"; -import { listBenchmarks } from "../services/benchmarkService.js"; +import { + listBenchmarks, + listPublicBenchmarks, +} from "../services/benchmarkService.js"; import type { Benchmark } from "../store/benchmarkStore.js"; export function BenchmarkListScreen() { const { exit: inkExit } = useApp(); - const { navigate, goBack } = useNavigation(); - const [selectedIndex, setSelectedIndex] = React.useState(0); + const { navigate, goBack, params, updateCurrentParams } = useNavigation(); + const [selectedIndex, setSelectedIndex] = React.useState( + params.selectedIndex ? Number(params.selectedIndex) : 0, + ); const [showPopup, setShowPopup] = React.useState(false); const [selectedOperation, setSelectedOperation] = React.useState(0); + const [tab, setTab] = React.useState<"private" | "public">( + params.tab === "public" ? "public" : "private", + ); + const isPublic = tab === "public"; + + // Save current tab and cursor into route params so back-navigation restores them + const saveListState = React.useCallback(() => { + updateCurrentParams({ + tab, + selectedIndex: String(selectedIndex), + }); + }, [updateCurrentParams, tab, selectedIndex]); // Search state const search = useListSearch({ @@ -41,7 +58,7 @@ export function BenchmarkListScreen() { }); // Calculate overhead for viewport height - const overhead = 13 + search.getSearchOverhead(); + const overhead = 14 + search.getSearchOverhead(); const { viewportHeight, terminalWidth } = useViewportHeight({ overhead, minHeight: 5, @@ -61,19 +78,27 @@ export function BenchmarkListScreen() { // Fetch function for pagination hook const fetchPage = React.useCallback( async (params: { limit: number; startingAt?: string }) => { - const result = await listBenchmarks({ + const listFn = isPublic ? listPublicBenchmarks : listBenchmarks; + const result = await listFn({ limit: params.limit, startingAfter: params.startingAt, search: search.submittedSearchQuery || undefined, }); + // Public tab: only show benchmarks with bmj_support metadata + const items = isPublic + ? result.benchmarks.filter((b) => b.metadata?.bmj_support === "true") + : result.benchmarks; + return { - items: result.benchmarks, + items, hasMore: result.hasMore, - totalCount: result.totalCount, + totalCount: isPublic + ? items.length + (result.hasMore ? 1 : 0) + : result.totalCount, }; }, - [search.submittedSearchQuery], + [search.submittedSearchQuery, isPublic], ); // Use the shared pagination hook @@ -94,7 +119,7 @@ export function BenchmarkListScreen() { getItemId: (benchmark: Benchmark) => benchmark.id, pollInterval: 5000, pollingEnabled: !showPopup && !search.searchMode, - deps: [PAGE_SIZE, search.submittedSearchQuery], + deps: [PAGE_SIZE, search.submittedSearchQuery, isPublic], }); // Operations for benchmarks @@ -108,7 +133,7 @@ export function BenchmarkListScreen() { }, { key: "create_job", - label: "Create Benchmark Job", + label: "Run Benchmark Job", color: colors.success, icon: figures.play, }, @@ -209,21 +234,25 @@ export function BenchmarkListScreen() { const operationKey = operations[selectedOperation].key; if (operationKey === "view_details") { + saveListState(); navigate("benchmark-detail", { benchmarkId: selectedBenchmark.id, }); } else if (operationKey === "create_job") { + saveListState(); navigate("benchmark-job-create", { initialBenchmarkIds: selectedBenchmark.id, }); } } else if (input === "v" && selectedBenchmark) { setShowPopup(false); + saveListState(); navigate("benchmark-detail", { benchmarkId: selectedBenchmark.id, }); - } else if (input === "c" && selectedBenchmark) { + } else if (input === "r" && selectedBenchmark) { setShowPopup(false); + saveListState(); navigate("benchmark-job-create", { initialBenchmarkIds: selectedBenchmark.id, }); @@ -258,19 +287,26 @@ export function BenchmarkListScreen() { prevPage(); setSelectedIndex(0); } else if (key.return && selectedBenchmark) { + saveListState(); navigate("benchmark-detail", { benchmarkId: selectedBenchmark.id, }); } else if (input === "a" && selectedBenchmark) { setShowPopup(true); setSelectedOperation(0); - } else if (input === "c" && selectedBenchmark) { - // Quick shortcut to create a job + } else if (input === "r" && selectedBenchmark) { + saveListState(); navigate("benchmark-job-create", { initialBenchmarkIds: selectedBenchmark.id, }); } else if (input === "/") { search.enterSearchMode(); + } else if (input === "1" && tab !== "private") { + setTab("private"); + setSelectedIndex(0); + } else if (input === "2" && tab !== "public") { + setTab("public"); + setSelectedIndex(0); } else if (key.escape) { if (search.handleEscape()) { return; @@ -287,7 +323,10 @@ export function BenchmarkListScreen() { items={[ { label: "Home" }, { label: "Benchmarks" }, - { label: "Benchmark Definitions", active: true }, + { + label: isPublic ? "Public Benchmarks" : "Benchmark Defs", + active: true, + }, ]} /> @@ -303,7 +342,10 @@ export function BenchmarkListScreen() { items={[ { label: "Home" }, { label: "Benchmarks" }, - { label: "Benchmark Definitions", active: true }, + { + label: isPublic ? "Public Benchmarks" : "Benchmark Defs", + active: true, + }, ]} /> @@ -318,10 +360,30 @@ export function BenchmarkListScreen() { items={[ { label: "Home" }, { label: "Benchmarks" }, - { label: "Benchmark Definitions", active: true }, + { + label: isPublic ? "Public Benchmarks" : "Benchmark Defs", + active: true, + }, ]} /> + {/* Tab indicator */} + + + [1] Private + + + + [2] Public + + + {/* Search bar */} {/* Table */} @@ -339,7 +403,7 @@ export function BenchmarkListScreen() { data={benchmarks} keyExtractor={(benchmark: Benchmark) => benchmark.id} selectedIndex={selectedIndex} - title={`benchmarks[${totalCount}]`} + title={`${isPublic ? "public " : ""}benchmarks[${totalCount}${hasMore ? "+" : ""}]`} columns={columns} emptyState={ @@ -354,12 +418,13 @@ export function BenchmarkListScreen() { {figures.hamburger} {totalCount} + {hasMore ? "+" : ""} {" "} total - {totalPages > 1 && ( + {(hasMore || hasPrev) && ( <> {" "} @@ -371,7 +436,8 @@ export function BenchmarkListScreen() { ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} + {!hasMore ? ` of ${totalPages}` : ""} )} @@ -381,7 +447,8 @@ export function BenchmarkListScreen() { •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} + {!hasMore ? ` of ${totalCount}` : ""} )} @@ -400,7 +467,7 @@ export function BenchmarkListScreen() { op.key === "view_details" ? "v" : op.key === "create_job" - ? "s" + ? "r" : "", }))} selectedOperation={selectedOperation} @@ -418,8 +485,9 @@ export function BenchmarkListScreen() { label: "Page", condition: hasMore || hasPrev, }, + { key: "1/2", label: "Private/Public" }, { key: "Enter", label: "Details" }, - { key: "c", label: "Create Job" }, + { key: "r", label: "Run Benchmark Job" }, { key: "a", label: "Actions" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, diff --git a/src/screens/BenchmarkRunDetailScreen.tsx b/src/screens/BenchmarkRunDetailScreen.tsx index d32ab94..61796d0 100644 --- a/src/screens/BenchmarkRunDetailScreen.tsx +++ b/src/screens/BenchmarkRunDetailScreen.tsx @@ -19,7 +19,7 @@ import { } from "../components/ResourceDetailPage.js"; import { getBenchmarkRun, - listScenarioRuns, + fetchAllScenarioRuns, } from "../services/benchmarkService.js"; import { useResourceDetail } from "../hooks/useResourceDetail.js"; import { SpinnerComponent } from "../components/Spinner.js"; @@ -60,17 +60,14 @@ export function BenchmarkRunDetailScreen({ const [scenarioRuns, setScenarioRuns] = React.useState([]); const [scenarioRunsLoading, setScenarioRunsLoading] = React.useState(false); - // Fetch scenario runs for this benchmark run + // Fetch all scenario runs for this benchmark run React.useEffect(() => { if (benchmarkRunId && !scenarioRunsLoading && scenarioRuns.length === 0) { setScenarioRunsLoading(true); - listScenarioRuns({ - limit: 10, // Show up to 10 scenarios - benchmarkRunId, - }) - .then((result) => { - setScenarioRuns(result.scenarioRuns); + fetchAllScenarioRuns(benchmarkRunId) + .then((runs) => { + setScenarioRuns(runs); setScenarioRunsLoading(false); }) .catch(() => { @@ -88,12 +85,9 @@ export function BenchmarkRunDetailScreen({ if (run.state !== "running") return; const interval = setInterval(() => { - listScenarioRuns({ - limit: 10, - benchmarkRunId, - }) - .then((result) => { - setScenarioRuns(result.scenarioRuns); + fetchAllScenarioRuns(benchmarkRunId) + .then((runs) => { + setScenarioRuns(runs); }) .catch(() => { // Silently fail @@ -304,8 +298,12 @@ export function BenchmarkRunDetailScreen({ ], }); - // Scenario Runs Section + // Scenario Runs Section (capped to avoid overflowing the terminal) if (scenarioRuns.length > 0) { + const MAX_EMBEDDED_SCENARIOS = 10; + const displayedScenarioRuns = scenarioRuns.slice(0, MAX_EMBEDDED_SCENARIOS); + const hasHiddenScenarios = scenarioRuns.length > MAX_EMBEDDED_SCENARIOS; + // Define columns for scenario table const scenarioColumns = [ createTextColumn("id", "ID", (s: ScenarioRun) => s.id, { @@ -362,13 +360,19 @@ export function BenchmarkRunDetailScreen({ { label: "", value: ( - + s.id} /> + {hasHiddenScenarios && ( + + Showing {MAX_EMBEDDED_SCENARIOS} of {scenarioRuns.length}{" "} + {"— press 's' to view all"} + + )} ), }, diff --git a/src/screens/BenchmarkRunListScreen.tsx b/src/screens/BenchmarkRunListScreen.tsx index dadd0d9..2f5f1dd 100644 --- a/src/screens/BenchmarkRunListScreen.tsx +++ b/src/screens/BenchmarkRunListScreen.tsx @@ -340,7 +340,7 @@ export function BenchmarkRunListScreen() { data={benchmarkRuns} keyExtractor={(run: BenchmarkRun) => run.id} selectedIndex={selectedIndex} - title={`benchmark_runs[${totalCount}]`} + title={`benchmark_runs[${totalCount}${hasMore ? "+" : ""}]`} columns={columns} emptyState={ @@ -355,12 +355,13 @@ export function BenchmarkRunListScreen() { {figures.hamburger} {totalCount} + {hasMore ? "+" : ""} {" "} total - {totalPages > 1 && ( + {(hasMore || hasPrev) && ( <> {" "} @@ -372,7 +373,8 @@ export function BenchmarkRunListScreen() { ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} + {!hasMore ? ` of ${totalPages}` : ""} )} @@ -382,7 +384,8 @@ export function BenchmarkRunListScreen() { •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} + {!hasMore ? ` of ${totalCount}` : ""} )} diff --git a/src/screens/ScenarioRunListScreen.tsx b/src/screens/ScenarioRunListScreen.tsx index df5543b..b3dbb0f 100644 --- a/src/screens/ScenarioRunListScreen.tsx +++ b/src/screens/ScenarioRunListScreen.tsx @@ -24,7 +24,10 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js"; import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js"; import { useCursorPagination } from "../hooks/useCursorPagination.js"; import { useListSearch } from "../hooks/useListSearch.js"; -import { listScenarioRuns } from "../services/benchmarkService.js"; +import { + listScenarioRuns, + fetchAllScenarioRuns, +} from "../services/benchmarkService.js"; import type { ScenarioRun } from "../store/benchmarkStore.js"; interface ScenarioRunListScreenProps { @@ -65,15 +68,66 @@ export function ScenarioRunListScreen({ const remainingWidth = terminalWidth - baseWidth; const nameWidth = Math.min(60, Math.max(15, remainingWidth)); - // Fetch function for pagination hook + // --- Client-side pagination (when benchmarkRunId is provided) --- + // Fetches all scenario runs upfront for accurate total count and cached page navigation. + const [allScenarioRuns, setAllScenarioRuns] = React.useState( + [], + ); + const [allRunsLoaded, setAllRunsLoaded] = React.useState(false); + const [allRunsError, setAllRunsError] = React.useState(null); + const [clientPage, setClientPage] = React.useState(0); + + React.useEffect(() => { + if (!benchmarkRunId) return; + let cancelled = false; + + fetchAllScenarioRuns(benchmarkRunId) + .then((runs) => { + if (!cancelled) { + setAllScenarioRuns(runs); + setAllRunsLoaded(true); + } + }) + .catch((err) => { + if (!cancelled) { + setAllRunsError(err as Error); + setAllRunsLoaded(true); + } + }); + + return () => { + cancelled = true; + }; + }, [benchmarkRunId]); + + // Poll to refresh the client-side cache + React.useEffect(() => { + if (!benchmarkRunId || showPopup || search.searchMode) return; + + const interval = setInterval(() => { + fetchAllScenarioRuns(benchmarkRunId) + .then((runs) => setAllScenarioRuns(runs)) + .catch(() => {}); + }, 5000); + + return () => clearInterval(interval); + }, [benchmarkRunId, showPopup, search.searchMode]); + + // Reset client page when page size changes + React.useEffect(() => { + if (benchmarkRunId) setClientPage(0); + }, [PAGE_SIZE, benchmarkRunId]); + + // --- Cursor-based pagination (when no benchmarkRunId) --- const fetchPage = React.useCallback( async (params: { limit: number; startingAt?: string }) => { + if (benchmarkRunId) { + return { items: [] as ScenarioRun[], hasMore: false }; + } const result = await listScenarioRuns({ limit: params.limit, startingAfter: params.startingAt, - benchmarkRunId, }); - return { items: result.scenarioRuns, hasMore: result.hasMore, @@ -83,28 +137,51 @@ export function ScenarioRunListScreen({ [benchmarkRunId], ); - // Use the shared pagination hook - const { - items: scenarioRuns, - loading, - navigating, - error, - currentPage, - hasMore, - hasPrev, - totalCount, - nextPage, - prevPage, - refresh, - } = useCursorPagination({ + const cursor = useCursorPagination({ fetchPage, pageSize: PAGE_SIZE, getItemId: (run: ScenarioRun) => run.id, - pollInterval: 5000, - pollingEnabled: !showPopup && !search.searchMode, + pollInterval: benchmarkRunId ? 0 : 5000, + pollingEnabled: !benchmarkRunId && !showPopup && !search.searchMode, deps: [PAGE_SIZE, benchmarkRunId], }); + // --- Unified pagination interface --- + const useClientSide = !!benchmarkRunId; + const clientPageItems = allScenarioRuns.slice( + clientPage * PAGE_SIZE, + (clientPage + 1) * PAGE_SIZE, + ); + + const scenarioRuns = useClientSide ? clientPageItems : cursor.items; + const loading = useClientSide + ? !allRunsLoaded && allScenarioRuns.length === 0 + : cursor.loading; + const navigating = useClientSide ? false : cursor.navigating; + const error = useClientSide ? allRunsError : cursor.error; + const currentPage = useClientSide ? clientPage : cursor.currentPage; + const hasMore = useClientSide + ? (clientPage + 1) * PAGE_SIZE < allScenarioRuns.length + : cursor.hasMore; + const hasPrev = useClientSide ? clientPage > 0 : cursor.hasPrev; + const totalCount = useClientSide ? allScenarioRuns.length : cursor.totalCount; + + const nextPage = () => { + if (useClientSide) { + setClientPage((p) => p + 1); + } else { + cursor.nextPage(); + } + }; + + const prevPage = () => { + if (useClientSide) { + setClientPage((p) => Math.max(0, p - 1)); + } else { + cursor.prevPage(); + } + }; + // Operations for scenario runs const operations: Operation[] = React.useMemo( () => [ @@ -191,6 +268,7 @@ export function ScenarioRunListScreen({ const selectedRun = scenarioRuns[selectedIndex]; // Calculate pagination info for display + const totalIsExact = useClientSide || !hasMore; const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)); const startIndex = currentPage * PAGE_SIZE; const endIndex = startIndex + scenarioRuns.length; @@ -328,7 +406,7 @@ export function ScenarioRunListScreen({ data={scenarioRuns} keyExtractor={(run: ScenarioRun) => run.id} selectedIndex={selectedIndex} - title={`scenario_runs[${totalCount}]`} + title={`scenario_runs[${totalCount}${!totalIsExact ? "+" : ""}]`} columns={columns} emptyState={ @@ -343,12 +421,13 @@ export function ScenarioRunListScreen({ {figures.hamburger} {totalCount} + {!totalIsExact ? "+" : ""} {" "} total - {totalPages > 1 && ( + {(hasMore || hasPrev) && ( <> {" "} @@ -360,7 +439,8 @@ export function ScenarioRunListScreen({ ) : ( - Page {currentPage + 1} of {totalPages} + Page {currentPage + 1} + {totalIsExact ? ` of ${totalPages}` : ""} )} @@ -370,7 +450,8 @@ export function ScenarioRunListScreen({ •{" "} - Showing {startIndex + 1}-{endIndex} of {totalCount} + Showing {startIndex + 1}-{endIndex} + {totalIsExact ? ` of ${totalCount}` : ""} {benchmarkRunId && ( <> diff --git a/src/services/benchmarkService.ts b/src/services/benchmarkService.ts index e637346..5c7f621 100644 --- a/src/services/benchmarkService.ts +++ b/src/services/benchmarkService.ts @@ -129,6 +129,29 @@ export async function listScenarioRuns( }; } +/** + * Fetch all scenario runs for a benchmark run, paginating through all results. + */ +export async function fetchAllScenarioRuns( + benchmarkRunId: string, +): Promise { + const allRuns: ScenarioRun[] = []; + let startingAfter: string | undefined; + + while (true) { + const result = await listScenarioRuns({ + limit: 100, + startingAfter, + benchmarkRunId, + }); + allRuns.push(...result.scenarioRuns); + if (!result.hasMore || result.scenarioRuns.length === 0) break; + startingAfter = result.scenarioRuns[result.scenarioRuns.length - 1].id; + } + + return allRuns; +} + /** * Get scenario run by ID */ diff --git a/src/store/navigationStateMachine.ts b/src/store/navigationStateMachine.ts index a67b468..9104737 100644 --- a/src/store/navigationStateMachine.ts +++ b/src/store/navigationStateMachine.ts @@ -105,6 +105,18 @@ export function reset(_state: NavigationState): NavigationState { }; } +/** + * Merge additional params into the current screen's params without navigating. + * Useful for preserving UI state (e.g. tab, cursor position) before navigating + * away, so that goBack() restores the screen with the correct state. + */ +export function updateCurrentParams( + state: NavigationState, + params: RouteParams, +): NavigationState { + return { ...state, params: { ...state.params, ...params } }; +} + /** * Whether there is a previous screen to go back to. */ diff --git a/src/store/navigationStore.tsx b/src/store/navigationStore.tsx index f9eb376..2628820 100644 --- a/src/store/navigationStore.tsx +++ b/src/store/navigationStore.tsx @@ -7,6 +7,7 @@ import { goBack as navGoBack, reset as navReset, canGoBack as navCanGoBack, + updateCurrentParams as navUpdateCurrentParams, } from "./navigationStateMachine.js"; import type { NavigationState } from "./navigationStateMachine.js"; @@ -91,6 +92,7 @@ interface NavigationContextValue { goBack: () => void; reset: () => void; canGoBack: () => boolean; + updateCurrentParams: (params: RouteParams) => void; } const NavigationContext = React.createContext( @@ -153,6 +155,10 @@ export function NavigationProvider({ [state.history.length], ); + const updateCurrentParams = React.useCallback((newParams: RouteParams) => { + setState((prev) => navUpdateCurrentParams(prev, newParams)); + }, []); + const value = React.useMemo( () => ({ currentScreen: state.currentScreen, @@ -163,6 +169,7 @@ export function NavigationProvider({ goBack, reset, canGoBack, + updateCurrentParams, }), [ state.currentScreen, @@ -173,6 +180,7 @@ export function NavigationProvider({ goBack, reset, canGoBack, + updateCurrentParams, ], ); diff --git a/tests/__tests__/store/navigationStateMachine.test.ts b/tests/__tests__/store/navigationStateMachine.test.ts index a5d8212..9949719 100644 --- a/tests/__tests__/store/navigationStateMachine.test.ts +++ b/tests/__tests__/store/navigationStateMachine.test.ts @@ -12,6 +12,7 @@ import { goBack, reset, canGoBack, + updateCurrentParams, } from "../../../src/store/navigationStateMachine.js"; import type { ScreenName, RouteParams } from "../../../src/store/navigationStore.js"; @@ -171,6 +172,31 @@ describe("navigationStateMachine", () => { }); }); + describe("updateCurrentParams", () => { + it("merges new params into current params without changing screen or history", () => { + let state = navigate(initialNavigationState, "benchmark-list" as ScreenName, { + benchmarkId: "bm_1", + } as RouteParams); + state = updateCurrentParams(state, { tab: "public", selectedIndex: "3" } as RouteParams); + expect(state.currentScreen).toBe("benchmark-list"); + expect(state.params.benchmarkId).toBe("bm_1"); + expect(state.params.tab).toBe("public"); + expect(state.params.selectedIndex).toBe("3"); + expect(state.history).toHaveLength(1); + }); + + it("updated params are captured in history on subsequent navigate", () => { + let state = navigate(initialNavigationState, "benchmark-list" as ScreenName, {}); + state = updateCurrentParams(state, { tab: "public" } as RouteParams); + state = navigate(state, "benchmark-detail" as ScreenName, { benchmarkId: "bm_1" } as RouteParams); + // History should contain the updated params from benchmark-list + expect(state.history[1]?.params.tab).toBe("public"); + state = goBack(state); + expect(state.currentScreen).toBe("benchmark-list"); + expect(state.params.tab).toBe("public"); + }); + }); + describe("canGoBack", () => { it("returns false when history is empty", () => { expect(canGoBack(initialNavigationState)).toBe(false);