From e4ed297c73f483e13643ae0d3a7313f2a9c30cc8 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Tue, 5 May 2026 12:12:27 -0700 Subject: [PATCH 1/5] FEAT: Render target capabilities as structured columns in config table - Add TargetCapabilitiesInfo model (6 boolean capability fields) to backend - Populate capabilities in target mapper from target_obj.capabilities - Add capability columns with checkmark/dismiss/dash indicators to TargetTable - Update Chat components to prefer capabilities object with legacy fallback - Add backend + frontend tests for capabilities population and rendering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/components/Chat/ChatInputArea.tsx | 2 +- frontend/src/components/Chat/ChatWindow.tsx | 4 +- .../components/Config/TargetTable.test.tsx | 38 ++++++++++++++- .../src/components/Config/TargetTable.tsx | 47 ++++++++++++++++++- frontend/src/types/index.ts | 10 ++++ pyrit/backend/mappers/target_mappers.py | 15 +++++- pyrit/backend/models/targets.py | 22 +++++++++ tests/unit/backend/test_mappers.py | 45 ++++++++++++++++++ 8 files changed, 176 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Chat/ChatInputArea.tsx b/frontend/src/components/Chat/ChatInputArea.tsx index 615244d0e9..ec3b638ea7 100644 --- a/frontend/src/components/Chat/ChatInputArea.tsx +++ b/frontend/src/components/Chat/ChatInputArea.tsx @@ -439,7 +439,7 @@ const ChatInputArea = forwardRef(functi />
- {activeTarget && activeTarget.supports_multi_turn === false && ( + {activeTarget && (activeTarget.capabilities?.supports_multi_turn ?? activeTarget.supports_multi_turn) === false && ( m.role === 'user') + const singleTurnLimitReached = (activeTarget?.capabilities?.supports_multi_turn ?? activeTarget?.supports_multi_turn) === false && messages.some(m => m.role === 'user') // Operator locking: if the loaded attack's operator differs from the current // user's operator label, the conversation should be read-only. @@ -561,7 +561,7 @@ export default function ChatWindow({ onBranchConversation={attackResultId && activeConversationId ? handleBranchConversation : undefined} onBranchAttack={activeTarget && activeConversationId ? handleBranchAttack : undefined} isLoading={isLoadingAttack || isLoadingMessages || awaitingConversationLoad} - isSingleTurn={activeTarget?.supports_multi_turn === false} + isSingleTurn={(activeTarget?.capabilities?.supports_multi_turn ?? activeTarget?.supports_multi_turn) === false} isOperatorLocked={isOperatorLocked} isCrossTarget={isCrossTargetLocked} noTargetSelected={!activeTarget} diff --git a/frontend/src/components/Config/TargetTable.test.tsx b/frontend/src/components/Config/TargetTable.test.tsx index 52618ef2ff..d5b0e94c63 100644 --- a/frontend/src/components/Config/TargetTable.test.tsx +++ b/frontend/src/components/Config/TargetTable.test.tsx @@ -17,12 +17,28 @@ const sampleTargets: TargetInstance[] = [ target_type: 'OpenAIChatTarget', endpoint: 'https://api.openai.com', model_name: 'gpt-4', + capabilities: { + supports_multi_turn: true, + supports_multi_message_pieces: true, + supports_json_schema: true, + supports_json_output: true, + supports_editable_history: true, + supports_system_prompt: true, + }, }, { target_registry_name: 'azure_image_dalle', target_type: 'AzureImageTarget', endpoint: 'https://azure.openai.com', model_name: 'dall-e-3', + capabilities: { + supports_multi_turn: false, + supports_multi_message_pieces: false, + supports_json_schema: false, + supports_json_output: false, + supports_editable_history: false, + supports_system_prompt: false, + }, }, { target_registry_name: 'text_target_basic', @@ -58,7 +74,7 @@ describe('TargetTable', () => { expect(screen.getAllByText('TextTarget').length).toBeGreaterThanOrEqual(1) }) - it('should display Type, Model, Endpoint and Parameters columns', () => { + it('should display Type, Model, Endpoint, capability columns and Parameters columns', () => { render( @@ -68,6 +84,12 @@ describe('TargetTable', () => { expect(screen.getByText('Type')).toBeInTheDocument() expect(screen.getByText('Model')).toBeInTheDocument() expect(screen.getByText('Endpoint')).toBeInTheDocument() + expect(screen.getByText('Multi-turn')).toBeInTheDocument() + expect(screen.getByText('Multi-piece')).toBeInTheDocument() + expect(screen.getByText('JSON Schema')).toBeInTheDocument() + expect(screen.getByText('JSON Output')).toBeInTheDocument() + expect(screen.getByText('Edit History')).toBeInTheDocument() + expect(screen.getByText('System Prompt')).toBeInTheDocument() expect(screen.getByText('Parameters')).toBeInTheDocument() }) @@ -151,10 +173,24 @@ describe('TargetTable', () => { ) + // Dashes for model, endpoint, and 6 capability columns (all unknown) const dashes = screen.getAllByText('—') expect(dashes.length).toBeGreaterThanOrEqual(2) }) + it('should show dash for capability columns when capabilities is absent', () => { + render( + + + + ) + + // TextTarget has no capabilities — all 6 should be dashes + const dashes = screen.getAllByText('—') + // model (—) + endpoint (—) + 6 capabilities (—) + params (—) = 9 + expect(dashes.length).toBeGreaterThanOrEqual(8) + }) + it('should display target_specific_params when present', () => { const targetWithParams: TargetInstance[] = [ { diff --git a/frontend/src/components/Config/TargetTable.tsx b/frontend/src/components/Config/TargetTable.tsx index 4b9011e33d..f7b9b9b575 100644 --- a/frontend/src/components/Config/TargetTable.tsx +++ b/frontend/src/components/Config/TargetTable.tsx @@ -12,7 +12,7 @@ import { Tooltip, Select, } from '@fluentui/react-components' -import { CheckmarkRegular } from '@fluentui/react-icons' +import { CheckmarkRegular, DismissRegular } from '@fluentui/react-icons' import type { TargetInstance } from '../../types' import { useTargetTableStyles } from './TargetTable.styles' @@ -39,6 +39,27 @@ function formatParams(params?: Record | null): string { return parts.join('\n') } +/** Capability column definitions with tooltip descriptions. */ +const CAPABILITY_COLUMNS = [ + { key: 'supports_multi_turn', label: 'Multi-turn', tooltip: 'Supports multi-turn conversation history' }, + { key: 'supports_multi_message_pieces', label: 'Multi-piece', tooltip: 'Supports multiple message pieces in a single request' }, + { key: 'supports_json_schema', label: 'JSON Schema', tooltip: 'Supports constraining output to a JSON schema' }, + { key: 'supports_json_output', label: 'JSON Output', tooltip: 'Supports JSON output format' }, + { key: 'supports_editable_history', label: 'Edit History', tooltip: 'Allows attack history to be modified' }, + { key: 'supports_system_prompt', label: 'System Prompt', tooltip: 'Supports system prompts' }, +] as const + +/** Render a capability indicator: ✓ (green) / ✗ (red) / — (unknown). */ +function CapabilityCell({ value }: { value: boolean | undefined }) { + if (value === undefined) { + return + } + if (value) { + return + } + return +} + /** Render the model cell with a tooltip when underlying model differs. */ function ModelCell({ target }: { target: TargetInstance }) { const displayName = target.model_name || '—' @@ -62,6 +83,21 @@ function ModelCell({ target }: { target: TargetInstance }) { return {displayName} } +/** Render capability cells for a target. */ +function CapabilityCells({ target }: { target: TargetInstance }) { + return ( + <> + {CAPABILITY_COLUMNS.map(({ key }) => ( + + + + ))} + + ) +} + export default function TargetTable({ targets, activeTarget, onSetActiveTarget }: TargetTableProps) { const styles = useTargetTableStyles() const [typeFilter, setTypeFilter] = useState('') @@ -99,6 +135,7 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget } {activeTarget.endpoint || '—'} + {formatParams(activeTarget.target_specific_params) || '—'} @@ -132,6 +169,13 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget } Type Model Endpoint + {CAPABILITY_COLUMNS.map(({ key, label, tooltip }) => ( + + + {label} + + + ))} Parameters @@ -167,6 +211,7 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget } {target.endpoint || '—'} + {formatParams(target.target_specific_params) || '—'} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 07d5865123..edf3ad1e74 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -53,6 +53,15 @@ export interface PaginationInfo { // --- Targets --- +export interface TargetCapabilitiesInfo { + supports_multi_turn: boolean + supports_multi_message_pieces: boolean + supports_json_schema: boolean + supports_json_output: boolean + supports_editable_history: boolean + supports_system_prompt: boolean +} + export interface TargetInstance { target_registry_name: string target_type: string @@ -63,6 +72,7 @@ export interface TargetInstance { top_p?: number | null max_requests_per_minute?: number | null supports_multi_turn?: boolean + capabilities?: TargetCapabilitiesInfo | null target_specific_params?: Record | null } diff --git a/pyrit/backend/mappers/target_mappers.py b/pyrit/backend/mappers/target_mappers.py index 1d72822690..50b6e7be83 100644 --- a/pyrit/backend/mappers/target_mappers.py +++ b/pyrit/backend/mappers/target_mappers.py @@ -5,7 +5,7 @@ Target mappers – domain → DTO translation for target-related models. """ -from pyrit.backend.models.targets import TargetInstance +from pyrit.backend.models.targets import TargetCapabilitiesInfo, TargetInstance from pyrit.prompt_target import PromptTarget @@ -43,6 +43,16 @@ def target_object_to_instance(target_registry_name: str, target_obj: PromptTarge extra = {k: v for k, v in params.items() if k not in extracted_keys and v is not None} combined_specific = {**extra, **explicit_specific} or None + caps = target_obj.capabilities + capabilities = TargetCapabilitiesInfo( + supports_multi_turn=caps.supports_multi_turn, + supports_multi_message_pieces=caps.supports_multi_message_pieces, + supports_json_schema=caps.supports_json_schema, + supports_json_output=caps.supports_json_output, + supports_editable_history=caps.supports_editable_history, + supports_system_prompt=caps.supports_system_prompt, + ) + return TargetInstance( target_registry_name=target_registry_name, target_type=identifier.class_name, @@ -52,6 +62,7 @@ def target_object_to_instance(target_registry_name: str, target_obj: PromptTarge temperature=params.get("temperature"), top_p=params.get("top_p"), max_requests_per_minute=params.get("max_requests_per_minute"), - supports_multi_turn=target_obj.capabilities.supports_multi_turn, + supports_multi_turn=caps.supports_multi_turn, + capabilities=capabilities, target_specific_params=combined_specific, ) diff --git a/pyrit/backend/models/targets.py b/pyrit/backend/models/targets.py index fef7cbe41e..81c12550f0 100644 --- a/pyrit/backend/models/targets.py +++ b/pyrit/backend/models/targets.py @@ -18,6 +18,27 @@ from pyrit.backend.models.common import PaginationInfo +class TargetCapabilitiesInfo(BaseModel): + """Structured capability flags for a target instance.""" + + supports_multi_turn: bool = Field(..., description="Whether the target supports multi-turn conversation history") + supports_multi_message_pieces: bool = Field( + ..., description="Whether the target supports multiple message pieces in a single request" + ) + supports_json_schema: bool = Field( + ..., description="Whether the target supports constraining output to a JSON schema" + ) + supports_json_output: bool = Field( + ..., description="Whether the target supports JSON output format" + ) + supports_editable_history: bool = Field( + ..., description="Whether the target allows the attack history to be modified" + ) + supports_system_prompt: bool = Field( + ..., description="Whether the target supports system prompts" + ) + + class TargetInstance(BaseModel): """ A runtime target instance. @@ -37,6 +58,7 @@ class TargetInstance(BaseModel): top_p: Optional[float] = Field(None, description="Top-p parameter for generation") max_requests_per_minute: Optional[int] = Field(None, description="Maximum requests per minute") supports_multi_turn: bool = Field(True, description="Whether the target supports multi-turn conversation history") + capabilities: Optional[TargetCapabilitiesInfo] = Field(None, description="Structured capability flags") target_specific_params: Optional[dict[str, Any]] = Field(None, description="Additional target-specific parameters") diff --git a/tests/unit/backend/test_mappers.py b/tests/unit/backend/test_mappers.py index 9eda90ab5b..45c505ccb8 100644 --- a/tests/unit/backend/test_mappers.py +++ b/tests/unit/backend/test_mappers.py @@ -1257,6 +1257,51 @@ def test_chat_target_extra_params_preserved(self) -> None: assert result.target_specific_params["seed"] == 42 assert result.target_specific_params["max_completion_tokens"] == 2048 + def test_capabilities_populated_from_target_object(self) -> None: + """Test that all 6 capability fields are populated from target_obj.capabilities.""" + target_obj = MagicMock(spec=PromptTarget) + target_obj.capabilities = TargetCapabilities( + supports_multi_turn=True, + supports_multi_message_pieces=True, + supports_json_schema=False, + supports_json_output=True, + supports_editable_history=False, + supports_system_prompt=True, + ) + mock_identifier = ComponentIdentifier( + class_name="OpenAIChatTarget", + class_module="pyrit.prompt_target", + params={"endpoint": "https://api.openai.com", "model_name": "gpt-4"}, + ) + target_obj.get_identifier.return_value = mock_identifier + + result = target_object_to_instance("t-1", target_obj) + + assert result.capabilities is not None + assert result.capabilities.supports_multi_turn is True + assert result.capabilities.supports_multi_message_pieces is True + assert result.capabilities.supports_json_schema is False + assert result.capabilities.supports_json_output is True + assert result.capabilities.supports_editable_history is False + assert result.capabilities.supports_system_prompt is True + + def test_capabilities_matches_legacy_supports_multi_turn(self) -> None: + """Test that legacy supports_multi_turn field matches capabilities.supports_multi_turn.""" + target_obj = MagicMock(spec=PromptTarget) + target_obj.capabilities = TargetCapabilities(supports_multi_turn=False) + mock_identifier = ComponentIdentifier( + class_name="TextTarget", + class_module="pyrit.prompt_target", + ) + target_obj.get_identifier.return_value = mock_identifier + + result = target_object_to_instance("t-1", target_obj) + + assert result.supports_multi_turn is False + assert result.capabilities is not None + assert result.capabilities.supports_multi_turn is False + assert result.supports_multi_turn == result.capabilities.supports_multi_turn + # ============================================================================ # Converter Mapper Tests From 4e6aa88af3aabf7b82852b44f72442c01bf45193 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Tue, 5 May 2026 12:56:39 -0700 Subject: [PATCH 2/5] FIX: Filter target_configuration from params, improve capability icons - Add target_configuration to extracted_keys so the verbose capabilities blob does not leak into the Parameters column - Make checkmark/dismiss icons 16px bold for better visibility - Add regression test for target_configuration filtering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/components/Config/TargetTable.tsx | 4 ++-- pyrit/backend/mappers/target_mappers.py | 2 ++ tests/unit/backend/test_mappers.py | 22 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Config/TargetTable.tsx b/frontend/src/components/Config/TargetTable.tsx index f7b9b9b575..61de5d7f43 100644 --- a/frontend/src/components/Config/TargetTable.tsx +++ b/frontend/src/components/Config/TargetTable.tsx @@ -55,9 +55,9 @@ function CapabilityCell({ value }: { value: boolean | undefined }) { return } if (value) { - return + return } - return + return } /** Render the model cell with a tooltip when underlying model differs. */ diff --git a/pyrit/backend/mappers/target_mappers.py b/pyrit/backend/mappers/target_mappers.py index 50b6e7be83..526e382a41 100644 --- a/pyrit/backend/mappers/target_mappers.py +++ b/pyrit/backend/mappers/target_mappers.py @@ -27,6 +27,7 @@ def target_object_to_instance(target_registry_name: str, target_obj: PromptTarge params = identifier.params # Keys that are extracted as top-level TargetInstance fields + # or are internal-only (target_configuration is the verbose capabilities blob). extracted_keys = { "endpoint", "model_name", @@ -36,6 +37,7 @@ def target_object_to_instance(target_registry_name: str, target_obj: PromptTarge "max_requests_per_minute", "supports_multi_turn", "target_specific_params", + "target_configuration", } # Collect remaining params as target_specific_params so the frontend can display them diff --git a/tests/unit/backend/test_mappers.py b/tests/unit/backend/test_mappers.py index 45c505ccb8..5eb26b2ea9 100644 --- a/tests/unit/backend/test_mappers.py +++ b/tests/unit/backend/test_mappers.py @@ -1302,6 +1302,28 @@ def test_capabilities_matches_legacy_supports_multi_turn(self) -> None: assert result.capabilities.supports_multi_turn is False assert result.supports_multi_turn == result.capabilities.supports_multi_turn + def test_target_configuration_excluded_from_target_specific_params(self) -> None: + """Test that the verbose target_configuration blob is filtered from target_specific_params.""" + target_obj = MagicMock(spec=PromptTarget) + target_obj.capabilities = TargetCapabilities(supports_multi_turn=True) + mock_identifier = ComponentIdentifier( + class_name="OpenAIChatTarget", + class_module="pyrit.prompt_target", + params={ + "endpoint": "https://api.openai.com", + "model_name": "gpt-4", + "target_configuration": {"capabilities": {"supports_multi_turn": True}}, + "reasoning_effort": "high", + }, + ) + target_obj.get_identifier.return_value = mock_identifier + + result = target_object_to_instance("t-1", target_obj) + + assert result.target_specific_params is not None + assert "target_configuration" not in result.target_specific_params + assert result.target_specific_params["reasoning_effort"] == "high" + # ============================================================================ # Converter Mapper Tests From d336751070e6df2966095747f2b70389810ef94b Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Tue, 5 May 2026 13:01:17 -0700 Subject: [PATCH 3/5] FEAT: Use filled circle icons, sticky table header - Switch to CheckmarkCircleFilled/DismissCircleFilled for better visibility - Add sticky header so column names stay visible when scrolling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- frontend/src/components/Config/TargetTable.styles.ts | 6 ++++++ frontend/src/components/Config/TargetTable.tsx | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Config/TargetTable.styles.ts b/frontend/src/components/Config/TargetTable.styles.ts index 6c02a7077c..b7ff07ca81 100644 --- a/frontend/src/components/Config/TargetTable.styles.ts +++ b/frontend/src/components/Config/TargetTable.styles.ts @@ -9,6 +9,12 @@ export const useTargetTableStyles = makeStyles({ tableLayout: 'fixed', width: '100%', }, + stickyHeader: { + position: 'sticky', + top: 0, + backgroundColor: tokens.colorNeutralBackground1, + zIndex: 1, + }, activeRow: { backgroundColor: tokens.colorBrandBackground2, }, diff --git a/frontend/src/components/Config/TargetTable.tsx b/frontend/src/components/Config/TargetTable.tsx index 61de5d7f43..a9b76a62ba 100644 --- a/frontend/src/components/Config/TargetTable.tsx +++ b/frontend/src/components/Config/TargetTable.tsx @@ -12,7 +12,7 @@ import { Tooltip, Select, } from '@fluentui/react-components' -import { CheckmarkRegular, DismissRegular } from '@fluentui/react-icons' +import { CheckmarkRegular, CheckmarkCircleFilled, DismissCircleFilled } from '@fluentui/react-icons' import type { TargetInstance } from '../../types' import { useTargetTableStyles } from './TargetTable.styles' @@ -55,9 +55,9 @@ function CapabilityCell({ value }: { value: boolean | undefined }) { return } if (value) { - return + return } - return + return } /** Render the model cell with a tooltip when underlying model differs. */ @@ -163,7 +163,7 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget } )} - + Type From e7bdfce4ff1b121e3cb228fe72659416e3a3acb7 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Tue, 5 May 2026 13:19:40 -0700 Subject: [PATCH 4/5] MAINT: Apply ruff formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyrit/backend/models/targets.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyrit/backend/models/targets.py b/pyrit/backend/models/targets.py index 81c12550f0..94a45ac234 100644 --- a/pyrit/backend/models/targets.py +++ b/pyrit/backend/models/targets.py @@ -28,15 +28,11 @@ class TargetCapabilitiesInfo(BaseModel): supports_json_schema: bool = Field( ..., description="Whether the target supports constraining output to a JSON schema" ) - supports_json_output: bool = Field( - ..., description="Whether the target supports JSON output format" - ) + supports_json_output: bool = Field(..., description="Whether the target supports JSON output format") supports_editable_history: bool = Field( ..., description="Whether the target allows the attack history to be modified" ) - supports_system_prompt: bool = Field( - ..., description="Whether the target supports system prompts" - ) + supports_system_prompt: bool = Field(..., description="Whether the target supports system prompts") class TargetInstance(BaseModel): From 4d617fa635047a64b3b0011b4d5b997f6121f097 Mon Sep 17 00:00:00 2001 From: Roman Lutz Date: Wed, 6 May 2026 15:49:35 -0700 Subject: [PATCH 5/5] FIX: Apply PR review feedback for TargetTable - Move capability icon colors and cell width into makeStyles using design tokens (colorPaletteGreenForeground1, colorPaletteRedForeground1); drop hardcoded fontSize so icons inherit. - Drop `Text size={200}` wrapper on table headers so they match other tables; switch tooltip trigger to a span with cursor:help class. - Add tooltips for Type, Model, Endpoint, and Parameters columns. - Tighten dash-count assertions to `toHaveLength(9)` and update inline comment to include the params dash. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../components/Config/TargetTable.styles.ts | 13 ++++++ .../components/Config/TargetTable.test.tsx | 6 +-- .../src/components/Config/TargetTable.tsx | 43 +++++++++++++++---- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Config/TargetTable.styles.ts b/frontend/src/components/Config/TargetTable.styles.ts index b7ff07ca81..35f7aca482 100644 --- a/frontend/src/components/Config/TargetTable.styles.ts +++ b/frontend/src/components/Config/TargetTable.styles.ts @@ -26,4 +26,17 @@ export const useTargetTableStyles = makeStyles({ whiteSpace: 'pre-line', wordBreak: 'break-word', }, + capabilityCell: { + width: '80px', + textAlign: 'center', + }, + capabilityIconSupported: { + color: tokens.colorPaletteGreenForeground1, + }, + capabilityIconUnsupported: { + color: tokens.colorPaletteRedForeground1, + }, + helpHeader: { + cursor: 'help', + }, }) diff --git a/frontend/src/components/Config/TargetTable.test.tsx b/frontend/src/components/Config/TargetTable.test.tsx index d5b0e94c63..2db78187a1 100644 --- a/frontend/src/components/Config/TargetTable.test.tsx +++ b/frontend/src/components/Config/TargetTable.test.tsx @@ -173,9 +173,9 @@ describe('TargetTable', () => { ) - // Dashes for model, endpoint, and 6 capability columns (all unknown) + // Dashes for model, endpoint, 6 capability columns (all unknown), and params const dashes = screen.getAllByText('—') - expect(dashes.length).toBeGreaterThanOrEqual(2) + expect(dashes).toHaveLength(9) }) it('should show dash for capability columns when capabilities is absent', () => { @@ -188,7 +188,7 @@ describe('TargetTable', () => { // TextTarget has no capabilities — all 6 should be dashes const dashes = screen.getAllByText('—') // model (—) + endpoint (—) + 6 capabilities (—) + params (—) = 9 - expect(dashes.length).toBeGreaterThanOrEqual(8) + expect(dashes).toHaveLength(9) }) it('should display target_specific_params when present', () => { diff --git a/frontend/src/components/Config/TargetTable.tsx b/frontend/src/components/Config/TargetTable.tsx index a9b76a62ba..373e84edd7 100644 --- a/frontend/src/components/Config/TargetTable.tsx +++ b/frontend/src/components/Config/TargetTable.tsx @@ -49,15 +49,23 @@ const CAPABILITY_COLUMNS = [ { key: 'supports_system_prompt', label: 'System Prompt', tooltip: 'Supports system prompts' }, ] as const +const COLUMN_TOOLTIPS = { + type: 'Target class implementation', + model: 'Configured model name. A dotted underline indicates the deployment alias differs from the underlying model — hover the value to see it.', + endpoint: 'API endpoint URL the target sends requests to', + parameters: 'Target-specific configuration parameters (e.g., reasoning_effort, max_output_tokens)', +} as const + /** Render a capability indicator: ✓ (green) / ✗ (red) / — (unknown). */ function CapabilityCell({ value }: { value: boolean | undefined }) { + const styles = useTargetTableStyles() if (value === undefined) { return } if (value) { - return + return } - return + return } /** Render the model cell with a tooltip when underlying model differs. */ @@ -85,10 +93,11 @@ function ModelCell({ target }: { target: TargetInstance }) { /** Render capability cells for a target. */ function CapabilityCells({ target }: { target: TargetInstance }) { + const styles = useTargetTableStyles() return ( <> {CAPABILITY_COLUMNS.map(({ key }) => ( - + @@ -166,17 +175,33 @@ export default function TargetTable({ targets, activeTarget, onSetActiveTarget } - Type - Model - Endpoint + + + Type + + + + + Model + + + + + Endpoint + + {CAPABILITY_COLUMNS.map(({ key, label, tooltip }) => ( - + - {label} + {label} ))} - Parameters + + + Parameters + +