Skip to content

Commit 9f2ec9e

Browse files
fix(agentic-workload-explorer): keep ?version= across session links
- The bare `/sessions/{id}` redirect now forwards `?version=` to the conversation tab. - `useExplorerHref` carries the current `?version=` into every in-explorer link (session rows, error and web-search links, session tabs, breadcrumbs), merging with existing queries and `#fragments` via `withVersion`. - Session tab activity now matches the explorer-relative path, since tab hrefs may carry a query string. - `versionQuery` moves to `paths.ts` so server redirect pages can use it. 中文:裸 `/sessions/{id}` 重定向及 explorer 内所有链接(会话列表、错误与 Web 搜索链接、 会话标签页、面包屑)现在都会保留 `?version=` 筛选;会话标签页的选中状态改为按 explorer 相对路径判断。 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent adfaa37 commit 9f2ec9e

9 files changed

Lines changed: 106 additions & 40 deletions

File tree

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import type { Metadata } from 'next';
22
import { redirect } from 'next/navigation';
33

4-
import { explorerHref } from '@/lib/agentic-workload-explorer/paths';
4+
import { explorerHref, versionQuery } from '@/lib/agentic-workload-explorer/paths';
55
import { explorerSessionMetadata } from '@/lib/agentic-workload-explorer/page-meta';
66

77
interface Props {
88
params: Promise<{ id: string }>;
9+
searchParams: Promise<Record<string, string | string[] | undefined>>;
910
}
1011

1112
export async function generateMetadata({ params }: Props): Promise<Metadata> {
1213
const { id } = await params;
1314
return explorerSessionMetadata(decodeURIComponent(id), 'conversation', 'en');
1415
}
1516

16-
/** A bare session URL opens its conversation tab. */
17-
export default async function ExplorerSessionPage({ params }: Props) {
18-
const { id } = await params;
19-
redirect(explorerHref(`/sessions/${id}/conversation`, 'en'));
17+
/** A bare session URL opens its conversation tab, keeping the trace-version filter. */
18+
export default async function ExplorerSessionPage({ params, searchParams }: Props) {
19+
const [{ id }, { version }] = await Promise.all([params, searchParams]);
20+
redirect(explorerHref(`/sessions/${id}/conversation`, 'en') + versionQuery(version));
2021
}
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import type { Metadata } from 'next';
22
import { redirect } from 'next/navigation';
33

4-
import { explorerHref } from '@/lib/agentic-workload-explorer/paths';
4+
import { explorerHref, versionQuery } from '@/lib/agentic-workload-explorer/paths';
55
import { explorerSessionMetadata } from '@/lib/agentic-workload-explorer/page-meta';
66

77
interface Props {
88
params: Promise<{ id: string }>;
9+
searchParams: Promise<Record<string, string | string[] | undefined>>;
910
}
1011

1112
export async function generateMetadata({ params }: Props): Promise<Metadata> {
1213
const { id } = await params;
1314
return explorerSessionMetadata(decodeURIComponent(id), 'conversation', 'zh');
1415
}
1516

16-
/** A bare session URL opens its conversation tab. */
17-
export default async function ZhExplorerSessionPage({ params }: Props) {
18-
const { id } = await params;
19-
redirect(explorerHref(`/sessions/${id}/conversation`, 'zh'));
17+
/** A bare session URL opens its conversation tab, keeping the trace-version filter. */
18+
export default async function ZhExplorerSessionPage({ params, searchParams }: Props) {
19+
const [{ id }, { version }] = await Promise.all([params, searchParams]);
20+
redirect(explorerHref(`/sessions/${id}/conversation`, 'zh') + versionQuery(version));
2021
}

‎packages/app/src/components/agentic-workload-explorer/__tests__/errors-page-retry.test.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { act, type AnchorHTMLAttributes, type ReactNode } from 'react';
44
import { createRoot, type Root } from 'react-dom/client';
55
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6+
import type * as NextNavigation from 'next/navigation';
67
import type { ErrorData } from '@/lib/agentic-workload-explorer/api-types';
78

89
const mocks = vi.hoisted(() => ({
@@ -25,6 +26,12 @@ vi.mock('next/link', () => ({
2526
),
2627
}));
2728

29+
// Rendered outside the App Router, so `useSearchParams` has no context.
30+
vi.mock('next/navigation', async (importOriginal) => ({
31+
...(await importOriginal<typeof NextNavigation>()),
32+
useSearchParams: () => new URLSearchParams(),
33+
}));
34+
2835
vi.mock('@/hooks/agentic-workload-explorer/use-trace-version', () => ({
2936
useTraceVersion: () => ({ apiParam: null }),
3037
appendTraceVersion: (url: string) => url,

‎packages/app/src/components/agentic-workload-explorer/section-nav.tsx‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import Link from 'next/link';
55
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
66
import { Suspense, useEffect, useMemo, useRef, useState } from 'react';
77

8-
import { explorerHref, explorerRelativePath } from '@/lib/agentic-workload-explorer/paths';
8+
import {
9+
explorerHref,
10+
explorerRelativePath,
11+
versionQuery,
12+
} from '@/lib/agentic-workload-explorer/paths';
913
import { track } from '@/lib/analytics';
1014
import type { Locale } from '@/lib/i18n';
1115
import { useLocale } from '@/lib/use-locale';
@@ -57,14 +61,6 @@ export function isActiveSection(relativePath: string | null, sectionPath: string
5761
return relativePath === sectionPath || relativePath.startsWith(`${sectionPath}/`);
5862
}
5963

60-
/**
61-
* Query string carried across section links. The trace-version selection
62-
* lives only in `?version=`, so dropping it would reset the filter.
63-
*/
64-
export function versionQuery(version: string | null): string {
65-
return version === null ? '' : `?version=${encodeURIComponent(version)}`;
66-
}
67-
6864
/** Number keys 1-9 jump to the first nine sections and 0 to the tenth. */
6965
function useSectionHotkeys(locale: Locale, query: string) {
7066
const router = useRouter();

‎packages/app/src/components/agentic-workload-explorer/views/session-detail-layout.tsx‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export default function SessionLayout({ children }: { children: React.ReactNode
224224
}
225225
const { session, requests } = data;
226226
const { usage } = session;
227-
const basePath = explorerHref(`/sessions/${id}`);
227+
const sessionPath = `/sessions/${id}`;
228228

229229
function selectReversed(next: boolean) {
230230
setReversed(next);
@@ -249,8 +249,12 @@ export default function SessionLayout({ children }: { children: React.ReactNode
249249
'radix-tree',
250250
];
251251
const navLinks = tabKeys.map((key) => ({
252-
href: `${basePath}/${key}`,
252+
href: explorerHref(`${sessionPath}/${key}`),
253253
label: EXPLORER_SESSION_TAB_META[key][locale],
254+
// Match on the explorer-relative path; `href` may carry `?version=`.
255+
active:
256+
relativePath === `${sessionPath}/${key}` ||
257+
(key === 'conversation' && relativePath === sessionPath),
254258
}));
255259

256260
return (
@@ -385,9 +389,7 @@ export default function SessionLayout({ children }: { children: React.ReactNode
385389
{/* Scrolls on its own on narrow screens instead of widening the page. */}
386390
<nav className="flex min-w-0 items-center gap-0.5 overflow-x-auto border border-border rounded-md p-0.5">
387391
{navLinks.map((link) => {
388-
const isActive =
389-
pathname === link.href ||
390-
(link.href.endsWith('/conversation') && pathname === basePath);
392+
const isActive = link.active;
391393
return (
392394
<Link
393395
key={link.href}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
'use client';
22

3+
import { useSearchParams } from 'next/navigation';
34
import { useCallback } from 'react';
45

5-
import { explorerHref } from '@/lib/agentic-workload-explorer/paths';
6+
import { explorerHref, withVersion } from '@/lib/agentic-workload-explorer/paths';
67
import { useLocale } from '@/lib/use-locale';
78

8-
/** Locale-aware `explorerHref` bound to the current page's language. */
9+
/**
10+
* Locale-aware `explorerHref` bound to the current page's language. Links
11+
* keep the current `?version=` so the trace-version filter survives
12+
* navigation inside the explorer.
13+
*/
914
export function useExplorerHref(): (path: string) => string {
1015
const locale = useLocale();
11-
return useCallback((path: string) => explorerHref(path, locale), [locale]);
16+
const version = useSearchParams().get('version');
17+
return useCallback(
18+
(path: string) => withVersion(explorerHref(path, locale), version),
19+
[locale, version],
20+
);
1221
}

‎packages/app/src/lib/agentic-workload-explorer/page-meta.test.ts‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import path from 'node:path';
44
import { SITE_URL } from '@semianalysisai/inferencex-constants';
55
import { describe, expect, it } from 'vitest';
66

7-
import {
8-
ALL_SECTIONS,
9-
isActiveSection,
10-
versionQuery,
11-
} from '@/components/agentic-workload-explorer/section-nav';
7+
import { ALL_SECTIONS, isActiveSection } from '@/components/agentic-workload-explorer/section-nav';
128

139
import {
1410
EXPLORER_SECTION_META,
@@ -86,10 +82,3 @@ describe('isActiveSection', () => {
8682
expect(isActiveSection(null, '/sessions')).toBe(false);
8783
});
8884
});
89-
90-
describe('versionQuery', () => {
91-
it('carries the trace-version selection across section links', () => {
92-
expect(versionQuery(null)).toBe('');
93-
expect(versionQuery('all')).toBe('?version=all');
94-
});
95-
});

‎packages/app/src/lib/agentic-workload-explorer/paths.test.ts‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { EXPLORER_API_BASE, EXPLORER_BASE_PATH, explorerHref, explorerRelativePath } from './paths';
3+
import {
4+
EXPLORER_API_BASE,
5+
EXPLORER_BASE_PATH,
6+
explorerHref,
7+
explorerRelativePath,
8+
versionQuery,
9+
withVersion,
10+
} from './paths';
411

512
describe('explorerHref', () => {
613
it('maps the explorer root to the base path in each locale', () => {
@@ -37,3 +44,32 @@ it('keeps the page and API prefixes aligned with the route directories', () => {
3744
expect(EXPLORER_BASE_PATH).toBe('/agentic-workload-explorer');
3845
expect(EXPLORER_API_BASE).toBe('/api/v1/agentic-workload-explorer');
3946
});
47+
48+
describe('versionQuery', () => {
49+
it('carries the trace-version selection across explorer links', () => {
50+
expect(versionQuery(null)).toBe('');
51+
expect(versionQuery(undefined)).toBe('');
52+
expect(versionQuery('all')).toBe('?version=all');
53+
expect(versionQuery(['all', '2'])).toBe('?version=all');
54+
});
55+
});
56+
57+
describe('withVersion', () => {
58+
it('leaves the href alone without a selection', () => {
59+
expect(withVersion('/agentic-workload-explorer/sessions', null)).toBe(
60+
'/agentic-workload-explorer/sessions',
61+
);
62+
});
63+
64+
it('merges into an existing query and keeps the fragment', () => {
65+
expect(withVersion('/agentic-workload-explorer/sessions/a/conversation#req-1', 'all')).toBe(
66+
'/agentic-workload-explorer/sessions/a/conversation?version=all#req-1',
67+
);
68+
expect(withVersion('/agentic-workload-explorer/sessions?sort=cost', 'all')).toBe(
69+
'/agentic-workload-explorer/sessions?sort=cost&version=all',
70+
);
71+
expect(withVersion('/agentic-workload-explorer/sessions?version=2', 'all')).toBe(
72+
'/agentic-workload-explorer/sessions?version=2',
73+
);
74+
});
75+
});

‎packages/app/src/lib/agentic-workload-explorer/paths.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,28 @@ export function explorerRelativePath(pathname: string): string | null {
2626
if (!enPath.startsWith(`${EXPLORER_BASE_PATH}/`)) return null;
2727
return enPath.slice(EXPLORER_BASE_PATH.length);
2828
}
29+
30+
/**
31+
* Query string carried across explorer links. The trace-version selection
32+
* lives only in `?version=`, so dropping it would reset the filter.
33+
*/
34+
export function versionQuery(version: string | string[] | null | undefined): string {
35+
const value = Array.isArray(version) ? version[0] : version;
36+
return value === null || value === undefined ? '' : `?version=${encodeURIComponent(value)}`;
37+
}
38+
39+
/**
40+
* Add the trace-version selection to an explorer href that may already carry
41+
* a query string or a `#fragment`. An explicit `version` in `href` wins.
42+
*/
43+
export function withVersion(href: string, version: string | null): string {
44+
if (version === null) return href;
45+
const hashAt = href.indexOf('#');
46+
const hash = hashAt === -1 ? '' : href.slice(hashAt);
47+
const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);
48+
const queryAt = beforeHash.indexOf('?');
49+
const path = queryAt === -1 ? beforeHash : beforeHash.slice(0, queryAt);
50+
const params = new URLSearchParams(queryAt === -1 ? '' : beforeHash.slice(queryAt + 1));
51+
if (!params.has('version')) params.set('version', version);
52+
return `${path}?${params.toString()}${hash}`;
53+
}

0 commit comments

Comments
 (0)