From 904b93ec827939d706faf1f8657065d8d90cf97d Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Sun, 3 May 2026 17:58:39 -0700 Subject: [PATCH 1/2] feat: add Open In Browser keybind to agents, axons, and benchmarks --- src/commands/agent/list.tsx | 5 +++ src/commands/axon/list.tsx | 5 +++ src/screens/AgentDetailScreen.tsx | 2 ++ src/screens/AxonDetailScreen.tsx | 2 ++ src/screens/BenchmarkDetailScreen.tsx | 2 ++ src/screens/BenchmarkListScreen.tsx | 5 +++ src/screens/BenchmarkRunDetailScreen.tsx | 2 ++ src/screens/BenchmarkRunListScreen.tsx | 7 ++++ src/screens/ScenarioRunDetailScreen.tsx | 2 ++ src/screens/ScenarioRunListScreen.tsx | 5 +++ src/utils/url.ts | 46 ++++++++++++++++++++++++ 11 files changed, 83 insertions(+) diff --git a/src/commands/agent/list.tsx b/src/commands/agent/list.tsx index 13a78e3f..eb3fc521 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 50280725..fb068ddc 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/screens/AgentDetailScreen.tsx b/src/screens/AgentDetailScreen.tsx index a8a98b9a..8f7c3e54 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 4515d82c..ccc6c36c 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 4bc7b334..71f24bb2 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 78cdf0bc..5bbe5c19 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 d32ab94a..0c6c05eb 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 7a093d72..38c92c80 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/ScenarioRunDetailScreen.tsx b/src/screens/ScenarioRunDetailScreen.tsx index a3c5a41f..57f83c84 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 38390aeb..40be8411 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 2ac450e1..6ba5b211 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 */ From d41440c6ecd4803a48b2a21c6b7943f6d258bf84 Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Mon, 11 May 2026 13:09:36 -0700 Subject: [PATCH 2/2] feat: add clipboard keybinds to detail screens - Add y keybind to copy resource name on all detail screens (via ResourceDetailPage) - Add h keybind to copy endpoint on MCP config detail screen (via extraKeybinds) - Enhanced clipboard feedback with descriptive labels (ID copied!, Name copied!, Endpoint copied!) - Add extraKeybinds/extraNavTips props to ResourceDetailPage for resource-specific shortcuts --- src/components/ResourceDetailPage.tsx | 14 +++++++++++--- src/components/resourceDetailTypes.ts | 7 +++++++ src/screens/McpConfigDetailScreen.tsx | 6 ++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/components/ResourceDetailPage.tsx b/src/components/ResourceDetailPage.tsx index e9d4333c..99a420ed 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 2954d682..f0711810 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/McpConfigDetailScreen.tsx b/src/screens/McpConfigDetailScreen.tsx index 8b1610e8..24826367 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" }]} /> ); }