diff --git a/packages/web/src/features/agents/agent-settings-page.tsx b/packages/web/src/features/agents/agent-settings-page.tsx index 041faa2..3ce0e38 100644 --- a/packages/web/src/features/agents/agent-settings-page.tsx +++ b/packages/web/src/features/agents/agent-settings-page.tsx @@ -10,7 +10,7 @@ */ import { useCallback, useEffect, useRef, useState } from "react"; import type { ChangeEvent } from "react"; -import { useNavigate, useParams } from "react-router"; +import { useNavigate, useParams, useSearchParams } from "react-router"; import type { AgentConfigResponse, AgentConfigUpdateRequest, @@ -72,6 +72,19 @@ function numToStr(n: number | undefined): string { return n === undefined ? "" : String(n); } +/** + * Narrow an untrusted `?tab=` query value to a known tab key (exported for unit tests): + * validated against the live TABS keys — not a hardcoded list — so newly added tabs + * deep-link without touching this helper; missing/unknown values fall back to the default. + */ +export function resolveTabKey( + raw: string | null, + tabs: ReadonlyArray<{ key: K }>, + fallback: K, +): K { + return tabs.some((t) => t.key === raw) ? (raw as K) : fallback; +} + export function AgentSettingsPage() { // Read inside the component: after a language switch remount, this picks up the current dictionary. const TABS = [ @@ -90,7 +103,27 @@ export function AgentSettingsPage() { const projectId = currentProject?.projectId ?? null; const [data, setData] = useState(null); - const [tab, setTab] = useState("overview"); + // ?tab= deep link (from the Agents page's stat icons): a valid key lands the page on that + // tab; missing/unknown values fall back to "overview", exactly the previous behavior. + const [searchParams, setSearchParams] = useSearchParams(); + const [tab, setTab] = useState(() => + resolveTabKey(searchParams.get("tab"), TABS, "overview"), + ); + /** Switch tab and mirror it into `?tab=` (replace history entry, keep other params) so the address stays shareable. */ + const switchTab = useCallback( + (next: TabKey) => { + setTab(next); + setSearchParams( + (prev) => { + const p = new URLSearchParams(prev); + p.set("tab", next); + return p; + }, + { replace: true }, + ); + }, + [setSearchParams], + ); // Only the initial config load failure renders inline (the page can't show without it); saves/imports report via toast. const [error, setError] = useState(null); @@ -185,7 +218,7 @@ export function AgentSettingsPage() {

{data.config.name ?? agentId}

{agentId}

- +
{tab === "overview" && ( { + setCurrentAgentId(agentId); + navigate(`/agents/${agentId}?tab=${tab}`); + }; + const doDelete = async () => { if (!projectId || !deleting) return; setBusy(true); @@ -197,7 +217,7 @@ export function AgentsPage() { (same line as the name); description/stats share the same left edge as the avatar (the column's left edge) */}
- {/* Title line: small avatar + name + agentId + active badge */} + {/* Title line: small avatar + name + agentId + version badge */}
v{a.version} - {a.activeSessionCount > 0 && ( - - {S.agent.activeSessions} {a.activeSessionCount} - - )}
{/* Description truncated to one line (an empty description still takes up a line, keeping card heights equal) */}

@@ -225,7 +240,10 @@ export function AgentsPage() {

{/* Stats on their own line: same color/font size as the description; each reserves a minimum width so they align vertically across cards; meaning - folded into the hover title */} + folded into the hover title. Tool/vault/schedule/skill counts are buttons + deep-linking to the matching settings tab (also for built-in Agents — + their Settings entry point has no gating either); session count and + last-modified stay plain text */}
{a.sessionCount} - openSettingsTab(a.agentId, "tools")} > {a.toolCount} - - + { + it("passes a known tab key through", () => { + expect(resolveTabKey("tools", TABS, "overview")).toBe("tools"); + expect(resolveTabKey("vault", TABS, "overview")).toBe("vault"); + }); + + it("falls back for absent, empty, unknown, or wrong-cased values", () => { + expect(resolveTabKey(null, TABS, "overview")).toBe("overview"); + expect(resolveTabKey("", TABS, "overview")).toBe("overview"); + expect(resolveTabKey("bogus", TABS, "overview")).toBe("overview"); + expect(resolveTabKey("Tools", TABS, "overview")).toBe("overview"); + }); + + it("validates against the supplied keys, not a hardcoded list", () => { + expect(resolveTabKey("skills", [...TABS, { key: "skills" }], "overview")).toBe("skills"); + }); +});