diff --git a/ui/src/panes/InspectorPane.test.tsx b/ui/src/panes/InspectorPane.test.tsx
new file mode 100644
index 00000000..bfe9439d
--- /dev/null
+++ b/ui/src/panes/InspectorPane.test.tsx
@@ -0,0 +1,222 @@
+// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
+//
+// SPDX-License-Identifier: MPL-2.0
+
+import { render, screen, fireEvent } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+import type { NodeDefinition } from '@/types/types';
+
+vi.mock('jotai/react', () => ({
+ useAtomValue: () => ({}),
+}));
+
+vi.mock('@/stores/sessionAtoms', () => ({
+ nodeParamsAtom: () => 'mock-atom',
+}));
+
+// Import after mocks are set up
+const { default: InspectorPane } = await import('./InspectorPane');
+
+const baseNodeDefinition: NodeDefinition = {
+ kind: 'test-node',
+ description: 'A test node',
+ inputs: [],
+ outputs: [],
+ param_schema: {},
+ categories: [],
+ bidirectional: false,
+};
+
+const baseNode = {
+ id: 'node-1',
+ type: 'custom',
+ position: { x: 0, y: 0 },
+ data: { label: 'TestNode', kind: 'test-node', params: {} },
+};
+
+describe('InspectorPane', () => {
+ let onParamChange: (nodeId: string, paramName: string, value: unknown) => void;
+ let onLabelChange: (nodeId: string, newLabel: string) => void;
+
+ beforeEach(() => {
+ onParamChange = vi.fn<(nodeId: string, paramName: string, value: unknown) => void>();
+ onLabelChange = vi.fn<(nodeId: string, newLabel: string) => void>();
+ });
+
+ it('renders a select dropdown for enum-constrained string properties', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ resolution: {
+ type: 'string',
+ enum: ['640x480', '1280x720', '1920x1080'],
+ description: 'Viewport resolution',
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const select = screen.getByRole('combobox', { name: 'Viewport resolution' });
+ expect(select).toBeInTheDocument();
+ expect(select).toHaveValue('1280x720');
+
+ const options = screen.getAllByRole('option');
+ expect(options).toHaveLength(3);
+ expect(options[0]).toHaveTextContent('640x480');
+ expect(options[1]).toHaveTextContent('1280x720');
+ expect(options[2]).toHaveTextContent('1920x1080');
+ });
+
+ it('falls back to first enum value when current value does not match any option', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ resolution: {
+ type: 'string',
+ enum: ['640x480', '1280x720', '1920x1080'],
+ description: 'Viewport resolution',
+ tunable: true,
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const select = screen.getByRole('combobox', { name: 'Viewport resolution' });
+ expect(select).toHaveValue('640x480');
+ });
+
+ it('falls back to first enum value when schema.default is not in enum', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ resolution: {
+ type: 'string',
+ enum: ['640x480', '1280x720'],
+ default: 'not-a-valid-option',
+ description: 'Viewport resolution',
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const select = screen.getByRole('combobox', { name: 'Viewport resolution' });
+ expect(select).toHaveValue('640x480');
+ });
+
+ it('sends UpdateParams on selection change', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ mode: {
+ type: 'string',
+ enum: ['fast', 'balanced', 'quality'],
+ description: 'Processing mode',
+ tunable: true,
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const select = screen.getByRole('combobox', { name: 'Processing mode' });
+ fireEvent.change(select, { target: { value: 'quality' } });
+
+ expect(onParamChange).toHaveBeenCalledWith('node-1', 'mode', 'quality');
+ });
+
+ it('renders a text input for non-enum string properties', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ title: {
+ type: 'string',
+ description: 'Display title',
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
+ expect(screen.getByRole('textbox', { name: 'Display title' })).toBeInTheDocument();
+ });
+
+ it('disables enum select for non-tunable params in monitor view', () => {
+ const definition = {
+ ...baseNodeDefinition,
+ param_schema: {
+ properties: {
+ resolution: {
+ type: 'string',
+ enum: ['640x480', '1280x720'],
+ description: 'Viewport resolution',
+ tunable: false,
+ },
+ },
+ },
+ };
+
+ render(
+
+ );
+
+ const select = screen.getByRole('combobox', { name: 'Viewport resolution' });
+ expect(select).toBeDisabled();
+ });
+});
diff --git a/ui/src/panes/InspectorPane.tsx b/ui/src/panes/InspectorPane.tsx
index 9442f81d..0bef5411 100644
--- a/ui/src/panes/InspectorPane.tsx
+++ b/ui/src/panes/InspectorPane.tsx
@@ -71,6 +71,28 @@ const FormInput = styled.input`
}
`;
+const FormSelect = styled.select`
+ width: 100%;
+ max-width: 100%;
+ padding: 8px;
+ background: var(--sk-panel-bg);
+ border: 1px solid var(--sk-border);
+ border-radius: 4px;
+ color: var(--sk-text);
+ box-sizing: border-box;
+ font-family: inherit;
+ cursor: pointer;
+
+ &:focus-visible {
+ outline: 1px solid var(--sk-primary);
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ }
+`;
+
const FormTextarea = styled.textarea`
width: 100%;
max-width: 100%;
@@ -183,6 +205,35 @@ const StringField: React.FC<{
);
};
+// Helper: Render select field for enum-constrained strings
+const SelectField: React.FC<{
+ inputId: string;
+ value: unknown;
+ schema: JsonSchemaProperty;
+ readOnly: boolean;
+ onChange: (value: string) => void;
+}> = ({ inputId, value, schema, readOnly, onChange }) => {
+ const options = (schema.enum ?? []).map(String);
+ const defaultStr = schema.default != null ? String(schema.default) : undefined;
+ const fallback = (defaultStr && options.includes(defaultStr) ? defaultStr : options[0]) ?? '';
+ const resolved = options.includes(String(value)) ? String(value) : fallback;
+ return (
+ onChange(e.target.value)}
+ disabled={readOnly}
+ aria-label={schema.description}
+ >
+ {options.map((opt) => (
+
+ ))}
+
+ );
+};
+
// Helper: Render number field
const NumberField: React.FC<{
inputId: string;
@@ -286,6 +337,17 @@ const InspectorPane: React.FC = ({
switch (schema.type) {
case 'string':
+ if (schema.enum && schema.enum.length > 0) {
+ return (
+ handleInputChange(key, v, schema)}
+ />
+ );
+ }
return (