diff --git a/src/commands/agent/list.tsx b/src/commands/agent/list.tsx index 13a78e3..eb3fc52 100644 --- a/src/commands/agent/list.tsx +++ b/src/commands/agent/list.tsx @@ -31,6 +31,8 @@ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; import { useCursorPagination } from "../../hooks/useCursorPagination.js"; import { useListSearch } from "../../hooks/useListSearch.js"; import { useNavigation } from "../../store/navigationStore.js"; +import { openInBrowser } from "../../utils/browser.js"; +import { getAgentUrl } from "../../utils/url.js"; interface ListOptions { full?: boolean; @@ -532,6 +534,8 @@ export const ListAgentsUI = ({ setSelectedOperation(0); } else if (input === "c" && activeTab === "private") { navigate("agent-create"); + } else if (input === "o" && selectedAgentItem) { + openInBrowser(getAgentUrl(selectedAgentItem.id)); } else if (input === "/") { search.enterSearchMode(); } else if (key.escape) { @@ -771,6 +775,7 @@ export const ListAgentsUI = ({ { key: "Tab", label: "Switch tab" }, { key: "Enter", label: "Details" }, { key: "a", label: "Actions" }, + { key: "o", label: "Browser" }, { key: "c", label: "Create", condition: activeTab === "private" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, diff --git a/src/commands/axon/list.tsx b/src/commands/axon/list.tsx index 5028072..fb068dd 100644 --- a/src/commands/axon/list.tsx +++ b/src/commands/axon/list.tsx @@ -22,6 +22,8 @@ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js"; import { useCursorPagination } from "../../hooks/useCursorPagination.js"; import { useListSearch } from "../../hooks/useListSearch.js"; import { useNavigation } from "../../store/navigationStore.js"; +import { openInBrowser } from "../../utils/browser.js"; +import { getAxonUrl } from "../../utils/url.js"; // ─── CLI ───────────────────────────────────────────────────────────────────── @@ -284,6 +286,8 @@ export const ListAxonsUI = ({ } else if (input === "a" && selectedAxonItem) { setShowPopup(true); setSelectedOperation(0); + } else if (input === "o" && selectedAxonItem) { + openInBrowser(getAxonUrl(selectedAxonItem.id)); } else if (input === "/") { search.enterSearchMode(); } else if (key.escape) { @@ -430,6 +434,7 @@ export const ListAxonsUI = ({ }, { key: "Enter", label: "Details" }, { key: "a", label: "Actions" }, + { key: "o", label: "Browser" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, ]} diff --git a/src/components/ResourceDetailPage.tsx b/src/components/ResourceDetailPage.tsx index e9d4333..99a420e 100644 --- a/src/components/ResourceDetailPage.tsx +++ b/src/components/ResourceDetailPage.tsx @@ -152,6 +152,8 @@ export function ResourceDetailPage({ onBack, buildDetailLines, additionalContent, + extraKeybinds, + extraNavTips, }: ResourceDetailPageProps) { const isMounted = React.useRef(true); const { navigate } = useNavigation(); @@ -167,9 +169,9 @@ export function ResourceDetailPage({ const [copyStatus, setCopyStatus] = React.useState(null); // Copy to clipboard with status feedback - const handleCopy = React.useCallback(async (text: string) => { + const handleCopy = React.useCallback(async (text: string, label?: string) => { const status = await copyToClipboard(text); - setCopyStatus(status); + setCopyStatus(label ? `${label} copied!` : status); setTimeout(() => setCopyStatus(null), 2000); }, []); @@ -393,7 +395,8 @@ export function ResourceDetailPage({ bindings: { q: onBack, escape: onBack, - c: () => handleCopy(getId(resource)), + c: () => handleCopy(getId(resource), "ID"), + y: () => handleCopy(getDisplayName(resource), "Name"), ...(buildDetailLines ? { i: () => { @@ -411,6 +414,7 @@ export function ResourceDetailPage({ }, enter: handleEnter, ...(getUrl ? { o: handleOpenInBrowser } : {}), + ...(extraKeybinds ? extraKeybinds({ copy: handleCopy }) : {}), }, onUnmatched: (input) => { // Operation shortcuts work from anywhere (all ops, including those in "View rest") @@ -452,6 +456,8 @@ export function ResourceDetailPage({ operationsStartIndex, onOperation, getId, + getDisplayName, + extraKeybinds, ], ); @@ -808,6 +814,8 @@ export function ResourceDetailPage({ : "Execute", }, { key: "c", label: "Copy ID" }, + { key: "y", label: "Copy Name" }, + ...(extraNavTips || []), { key: "i", label: "Full Details", condition: !!buildDetailLines }, { key: "o", label: "Browser", condition: !!getUrl }, { key: "q/Ctrl+C", label: "Back/Quit" }, diff --git a/src/components/resourceDetailTypes.ts b/src/components/resourceDetailTypes.ts index 2954d68..f071181 100644 --- a/src/components/resourceDetailTypes.ts +++ b/src/components/resourceDetailTypes.ts @@ -3,6 +3,7 @@ */ import React from "react"; import type { ScreenName, RouteParams } from "../store/navigationStore.js"; +import type { NavigationTip } from "./NavigationTips.js"; // --------------------------------------------------------------------------- // Detail field types @@ -119,4 +120,10 @@ export interface ResourceDetailPageProps { buildDetailLines?: (resource: T) => React.ReactElement[]; /** Optional: Additional content to render after details section */ additionalContent?: React.ReactNode; + /** Optional: Extra keybinds added to the main view. Receives a copy helper for clipboard feedback. */ + extraKeybinds?: (helpers: { + copy: (text: string, label?: string) => void; + }) => Record void>; + /** Optional: Extra navigation tips shown in the footer alongside the standard ones */ + extraNavTips?: NavigationTip[]; } diff --git a/src/screens/AgentDetailScreen.tsx b/src/screens/AgentDetailScreen.tsx index a8a98b9..8f7c3e5 100644 --- a/src/screens/AgentDetailScreen.tsx +++ b/src/screens/AgentDetailScreen.tsx @@ -22,6 +22,7 @@ import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js"; import { colors } from "../utils/theme.js"; +import { getAgentUrl } from "../utils/url.js"; interface AgentDetailScreenProps { agentId?: string; @@ -332,6 +333,7 @@ export function AgentDetailScreen({ agentId }: AgentDetailScreenProps) { resourceType="Agents" getDisplayName={(a) => a.name} getId={(a) => a.id} + getUrl={(a) => getAgentUrl(a.id)} getStatus={() => (agent.is_public ? "public" : "private")} detailSections={detailSections} operations={operations} diff --git a/src/screens/AxonDetailScreen.tsx b/src/screens/AxonDetailScreen.tsx index 4515d82..ccc6c36 100644 --- a/src/screens/AxonDetailScreen.tsx +++ b/src/screens/AxonDetailScreen.tsx @@ -16,6 +16,7 @@ import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; import { colors } from "../utils/theme.js"; +import { getAxonUrl } from "../utils/url.js"; interface AxonDetailScreenProps { axonId?: string; @@ -143,6 +144,7 @@ export function AxonDetailScreen({ axonId }: AxonDetailScreenProps) { resourceType="Axons" getDisplayName={(a) => a.name ?? a.id} getId={(a) => a.id} + getUrl={(a) => getAxonUrl(a.id)} getStatus={() => "active"} detailSections={detailSections} operations={[]} diff --git a/src/screens/BenchmarkDetailScreen.tsx b/src/screens/BenchmarkDetailScreen.tsx index 4bc7b33..71f24bb 100644 --- a/src/screens/BenchmarkDetailScreen.tsx +++ b/src/screens/BenchmarkDetailScreen.tsx @@ -18,6 +18,7 @@ import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb } from "../components/Breadcrumb.js"; import { colors } from "../utils/theme.js"; +import { getBenchmarkUrl } from "../utils/url.js"; interface BenchmarkDetailScreenProps { benchmarkId?: string; @@ -275,6 +276,7 @@ export function BenchmarkDetailScreen({ resourceType="Benchmark Definitions" getDisplayName={(b) => b.name || b.id} getId={(b) => b.id} + getUrl={(b) => getBenchmarkUrl(b.id, !!b.is_public)} getStatus={(b) => (b as any).status} detailSections={detailSections} operations={operations} diff --git a/src/screens/BenchmarkListScreen.tsx b/src/screens/BenchmarkListScreen.tsx index 78cdf0b..5bbe5c1 100644 --- a/src/screens/BenchmarkListScreen.tsx +++ b/src/screens/BenchmarkListScreen.tsx @@ -29,6 +29,8 @@ import { listPublicBenchmarks, } from "../services/benchmarkService.js"; import type { Benchmark } from "../store/benchmarkStore.js"; +import { openInBrowser } from "../utils/browser.js"; +import { getBenchmarkUrl } from "../utils/url.js"; export function BenchmarkListScreen() { const { exit: inkExit } = useApp(); @@ -281,6 +283,8 @@ export function BenchmarkListScreen() { } else if (input === "a" && selectedBenchmark) { setShowPopup(true); setSelectedOperation(0); + } else if (input === "o" && selectedBenchmark) { + openInBrowser(getBenchmarkUrl(selectedBenchmark.id, showPublic)); } else if (input === "s" && selectedBenchmark) { // Quick shortcut to create a job navigate("benchmark-job-create", { @@ -463,6 +467,7 @@ export function BenchmarkListScreen() { { key: "Enter", label: "Details" }, { key: "s", label: "Create Job" }, { key: "a", label: "Actions" }, + { key: "o", label: "Browser" }, { key: "Tab", label: "Switch tab" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, diff --git a/src/screens/BenchmarkRunDetailScreen.tsx b/src/screens/BenchmarkRunDetailScreen.tsx index d32ab94..0c6c05e 100644 --- a/src/screens/BenchmarkRunDetailScreen.tsx +++ b/src/screens/BenchmarkRunDetailScreen.tsx @@ -32,6 +32,7 @@ import { createComponentColumn, } from "../components/Table.js"; import { colors } from "../utils/theme.js"; +import { getBenchmarkRunUrl } from "../utils/url.js"; interface BenchmarkRunDetailScreenProps { benchmarkRunId?: string; @@ -621,6 +622,7 @@ export function BenchmarkRunDetailScreen({ resourceType="Benchmark Runs" getDisplayName={(r) => r.name || r.id} getId={(r) => r.id} + getUrl={(r) => getBenchmarkRunUrl(r.id, r.benchmark_id)} getStatus={(r) => r.state} detailSections={detailSections} operations={operations} diff --git a/src/screens/BenchmarkRunListScreen.tsx b/src/screens/BenchmarkRunListScreen.tsx index 7a093d7..38c92c8 100644 --- a/src/screens/BenchmarkRunListScreen.tsx +++ b/src/screens/BenchmarkRunListScreen.tsx @@ -26,6 +26,8 @@ import { useCursorPagination } from "../hooks/useCursorPagination.js"; import { useListSearch } from "../hooks/useListSearch.js"; import { listBenchmarkRuns } from "../services/benchmarkService.js"; import type { BenchmarkRun } from "../store/benchmarkStore.js"; +import { openInBrowser } from "../utils/browser.js"; +import { getBenchmarkRunUrl } from "../utils/url.js"; export function BenchmarkRunListScreen() { const { exit: inkExit } = useApp(); @@ -272,6 +274,10 @@ export function BenchmarkRunListScreen() { } else if (input === "a" && selectedRun) { setShowPopup(true); setSelectedOperation(0); + } else if (input === "o" && selectedRun) { + openInBrowser( + getBenchmarkRunUrl(selectedRun.id, selectedRun.benchmark_id), + ); } else if (input === "j") { // Quick shortcut to create a new job navigate("benchmark-job-create"); @@ -433,6 +439,7 @@ export function BenchmarkRunListScreen() { { key: "Enter", label: "Details" }, { key: "j", label: "New Job" }, { key: "a", label: "Actions" }, + { key: "o", label: "Browser" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, ]} diff --git a/src/screens/McpConfigDetailScreen.tsx b/src/screens/McpConfigDetailScreen.tsx index 8b1610e..2482636 100644 --- a/src/screens/McpConfigDetailScreen.tsx +++ b/src/screens/McpConfigDetailScreen.tsx @@ -368,6 +368,12 @@ export function McpConfigDetailScreen({ onOperation={handleOperation} onBack={goBack} buildDetailLines={buildDetailLines} + extraKeybinds={({ copy }) => ({ + h: () => { + copy(config.endpoint, "Endpoint"); + }, + })} + extraNavTips={[{ key: "h", label: "Copy Endpoint" }]} /> ); } diff --git a/src/screens/ScenarioRunDetailScreen.tsx b/src/screens/ScenarioRunDetailScreen.tsx index a3c5a41..57f83c8 100644 --- a/src/screens/ScenarioRunDetailScreen.tsx +++ b/src/screens/ScenarioRunDetailScreen.tsx @@ -22,6 +22,7 @@ import { SpinnerComponent } from "../components/Spinner.js"; import { ErrorMessage } from "../components/ErrorMessage.js"; import { Breadcrumb, type BreadcrumbItem } from "../components/Breadcrumb.js"; import { colors } from "../utils/theme.js"; +import { getScenarioRunUrl } from "../utils/url.js"; interface ScenarioRunDetailScreenProps { scenarioRunId?: string; @@ -340,6 +341,7 @@ export function ScenarioRunDetailScreen({ resourceType="Scenario Runs" getDisplayName={(r) => r.name || r.id} getId={(r) => r.id} + getUrl={(r) => getScenarioRunUrl(r.scenario_id, r.id)} getStatus={(r) => r.state} detailSections={detailSections} operations={operations} diff --git a/src/screens/ScenarioRunListScreen.tsx b/src/screens/ScenarioRunListScreen.tsx index 38390ae..40be841 100644 --- a/src/screens/ScenarioRunListScreen.tsx +++ b/src/screens/ScenarioRunListScreen.tsx @@ -26,6 +26,8 @@ import { useCursorPagination } from "../hooks/useCursorPagination.js"; import { useListSearch } from "../hooks/useListSearch.js"; import { listScenarioRuns } from "../services/benchmarkService.js"; import type { ScenarioRun } from "../store/benchmarkStore.js"; +import { openInBrowser } from "../utils/browser.js"; +import { getScenarioRunUrl } from "../utils/url.js"; interface ScenarioRunListScreenProps { benchmarkRunId?: string; @@ -269,6 +271,8 @@ export function ScenarioRunListScreen({ } else if (input === "a" && selectedRun) { setShowPopup(true); setSelectedOperation(0); + } else if (input === "o" && selectedRun) { + openInBrowser(getScenarioRunUrl(selectedRun.scenario_id, selectedRun.id)); } else if (input === "/") { search.enterSearchMode(); } else if (key.escape) { @@ -424,6 +428,7 @@ export function ScenarioRunListScreen({ }, { key: "Enter", label: "Details" }, { key: "a", label: "Actions" }, + { key: "o", label: "Browser" }, { key: "/", label: "Search" }, { key: "Esc", label: "Back" }, ]} diff --git a/src/utils/url.ts b/src/utils/url.ts index 2ac450e..6ba5b21 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -18,6 +18,52 @@ export function getBlueprintUrl(blueprintId: string): string { return `${platformBaseUrl()}/blueprints/${blueprintId}`; } +/** + * Generate an agent URL for the given agent ID + */ +export function getAgentUrl(agentId: string): string { + return `${platformBaseUrl()}/agents/${agentId}`; +} + +/** + * Generate an axon URL for the given axon ID + */ +export function getAxonUrl(axonId: string): string { + return `${platformBaseUrl()}/axons/${axonId}`; +} + +/** + * Generate a benchmark URL for the given benchmark ID + */ +export function getBenchmarkUrl( + benchmarkId: string, + isPublic: boolean, +): string { + const segment = isPublic ? "public" : "custom"; + return `${platformBaseUrl()}/benchmarks/${segment}/${benchmarkId}`; +} + +/** + * Generate a benchmark run URL for the given benchmark run ID + */ +export function getBenchmarkRunUrl( + benchmarkRunId: string, + benchmarkId?: string | null, +): string { + const bmSegment = benchmarkId ?? "single"; + return `${platformBaseUrl()}/benchmarks/custom/${bmSegment}/runs/${benchmarkRunId}`; +} + +/** + * Generate a scenario run URL for the given scenario and run IDs + */ +export function getScenarioRunUrl( + scenarioId: string, + scenarioRunId: string, +): string { + return `${platformBaseUrl()}/scenarios/${scenarioId}/runs/${scenarioRunId}`; +} + /** * Generate a settings URL */