From 44d4df12544a02572e5c1c68f70f73c80795fb29 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Mar 2026 15:10:41 -0400 Subject: [PATCH 1/4] fix(ui): restore scrolling in bounded code editors --- .../provider-editor/raw-editor-section.tsx | 5 +- .../raw-json-settings-editor-panel.tsx | 12 +++-- .../config-form/raw-editor-section.tsx | 5 +- .../profiles/editor/raw-editor-section.tsx | 5 +- ui/src/components/shared/code-editor.tsx | 53 +++++++++++-------- ui/src/components/shared/settings-dialog.tsx | 3 +- ui/src/pages/cliproxy-ai-providers.tsx | 2 + 7 files changed, 53 insertions(+), 32 deletions(-) diff --git a/ui/src/components/cliproxy/provider-editor/raw-editor-section.tsx b/ui/src/components/cliproxy/provider-editor/raw-editor-section.tsx index bae26325..904a4dcc 100644 --- a/ui/src/components/cliproxy/provider-editor/raw-editor-section.tsx +++ b/ui/src/components/cliproxy/provider-editor/raw-editor-section.tsx @@ -32,7 +32,7 @@ export function RawEditorSection({ } > -
+
{!isRawJsonValid && rawJsonEdits !== null && (
@@ -56,13 +56,14 @@ export function RawEditorSection({
)} -
+
diff --git a/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx b/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx index 6eec468b..0a84103a 100644 --- a/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx +++ b/ui/src/components/compatible-cli/raw-json-settings-editor-panel.tsx @@ -85,15 +85,21 @@ export function RawJsonSettingsEditorPanel({ Loading settings.json...
) : ( -
+
{parseWarning && (
Parse warning: {parseWarning}
)} -
+
- +
diff --git a/ui/src/components/copilot/config-form/raw-editor-section.tsx b/ui/src/components/copilot/config-form/raw-editor-section.tsx index 6a13c9e1..b1369951 100644 --- a/ui/src/components/copilot/config-form/raw-editor-section.tsx +++ b/ui/src/components/copilot/config-form/raw-editor-section.tsx @@ -40,7 +40,7 @@ export function RawEditorSection({
} > -
+
{!isRawJsonValid && rawJsonEdits !== null && (
@@ -63,13 +63,14 @@ export function RawEditorSection({
)} -
+
diff --git a/ui/src/components/profiles/editor/raw-editor-section.tsx b/ui/src/components/profiles/editor/raw-editor-section.tsx index d4bbe4ac..686fb2e7 100644 --- a/ui/src/components/profiles/editor/raw-editor-section.tsx +++ b/ui/src/components/profiles/editor/raw-editor-section.tsx @@ -41,7 +41,7 @@ export function RawEditorSection({
} > -
+
{!isRawJsonValid && rawJsonEdits !== null && (
@@ -64,13 +64,14 @@ export function RawEditorSection({
)} -
+
diff --git a/ui/src/components/shared/code-editor.tsx b/ui/src/components/shared/code-editor.tsx index 47f95edc..e1dd049e 100644 --- a/ui/src/components/shared/code-editor.tsx +++ b/ui/src/components/shared/code-editor.tsx @@ -20,6 +20,7 @@ interface CodeEditorProps { readonly?: boolean; className?: string; minHeight?: string; + heightMode?: 'content' | 'fill-parent'; } interface ValidationResult { @@ -70,10 +71,12 @@ export function CodeEditor({ readonly = false, className, minHeight = '300px', + heightMode = 'content', }: CodeEditorProps) { const { isDark } = useTheme(); const [isFocused, setIsFocused] = useState(false); const [isMasked, setIsMasked] = useState(true); + const isFillParent = heightMode === 'fill-parent'; // Validate on every change for JSON const validation = useMemo(() => { @@ -153,7 +156,7 @@ export function CodeEditor({ ); return ( -
+
{/* Editor container */}
- {} : onChange} - highlight={highlightCode} - key={isDark ? 'dark-editor' : 'light-editor'} - padding={12} - disabled={readonly} - onFocus={() => setIsFocused(true)} - onBlur={() => setIsFocused(false)} - textareaClassName={cn( - 'focus:outline-none font-mono text-sm', - readonly && 'cursor-not-allowed' - )} - preClassName="font-mono text-sm" - style={{ - fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', - fontSize: '0.875rem', - minHeight, - }} - /> +
+ {} : onChange} + highlight={highlightCode} + key={isDark ? 'dark-editor' : 'light-editor'} + padding={12} + disabled={readonly} + onFocus={() => setIsFocused(true)} + onBlur={() => setIsFocused(false)} + textareaClassName={cn( + 'focus:outline-none font-mono text-sm', + readonly && 'cursor-not-allowed' + )} + preClassName="font-mono text-sm" + style={{ + fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', + fontSize: '0.875rem', + minHeight, + }} + /> +
{/* Secrets Toggle Overlay */}
diff --git a/ui/src/components/shared/settings-dialog.tsx b/ui/src/components/shared/settings-dialog.tsx index faeffb54..dac1321d 100644 --- a/ui/src/components/shared/settings-dialog.tsx +++ b/ui/src/components/shared/settings-dialog.tsx @@ -279,7 +279,7 @@ function SettingsDialogContent({ - + @@ -293,6 +293,7 @@ function SettingsDialogContent({ onChange={handleRawJsonChange} language="json" minHeight="calc(60vh - 120px)" + heightMode="fill-parent" /> diff --git a/ui/src/pages/cliproxy-ai-providers.tsx b/ui/src/pages/cliproxy-ai-providers.tsx index f7a492f2..b7a8c79e 100644 --- a/ui/src/pages/cliproxy-ai-providers.tsx +++ b/ui/src/pages/cliproxy-ai-providers.tsx @@ -1248,6 +1248,7 @@ function EntryInspector({ onChange={handleRawJsonChange} language="json" minHeight="100%" + heightMode="fill-parent" />
@@ -1271,6 +1272,7 @@ function EntryInspector({ language="json" readonly minHeight="100%" + heightMode="fill-parent" />
From 823cf96e7788cc9f69d65b97688c56df84e6b632 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Mar 2026 15:10:48 -0400 Subject: [PATCH 2/4] test(ui): cover bounded code editor scroll mode --- .../ui/code-editor-usage-contract.test.ts | 42 +++++++++++++ .../unit/components/ui/code-editor.test.tsx | 60 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 ui/tests/unit/components/ui/code-editor-usage-contract.test.ts create mode 100644 ui/tests/unit/components/ui/code-editor.test.tsx diff --git a/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts b/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts new file mode 100644 index 00000000..7175b8d8 --- /dev/null +++ b/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts @@ -0,0 +1,42 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const boundedConsumers = [ + { + file: 'src/pages/cliproxy-ai-providers.tsx', + expectedCount: 2, + }, + { + file: 'src/components/cliproxy/provider-editor/raw-editor-section.tsx', + expectedCount: 1, + }, + { + file: 'src/components/profiles/editor/raw-editor-section.tsx', + expectedCount: 1, + }, + { + file: 'src/components/copilot/config-form/raw-editor-section.tsx', + expectedCount: 1, + }, + { + file: 'src/components/compatible-cli/raw-json-settings-editor-panel.tsx', + expectedCount: 1, + }, + { + file: 'src/components/shared/settings-dialog.tsx', + expectedCount: 1, + }, +] as const; + +describe('bounded CodeEditor consumers', () => { + it.each(boundedConsumers)('$file opts into fill-parent mode for every bounded editor', ({ + file, + expectedCount, + }) => { + const source = readFileSync(resolve(process.cwd(), file), 'utf8'); + const matches = source.match(/heightMode="fill-parent"/g) ?? []; + + expect(matches).toHaveLength(expectedCount); + }); +}); diff --git a/ui/tests/unit/components/ui/code-editor.test.tsx b/ui/tests/unit/components/ui/code-editor.test.tsx new file mode 100644 index 00000000..3a8d71cc --- /dev/null +++ b/ui/tests/unit/components/ui/code-editor.test.tsx @@ -0,0 +1,60 @@ +import { describe, expect, it, vi } from 'vitest'; +import { render, screen } from '@tests/setup/test-utils'; + +import { CodeEditor } from '@/components/shared/code-editor'; + +vi.mock('@/hooks/use-theme', () => ({ + useTheme: () => ({ isDark: false }), +})); + +describe('CodeEditor', () => { + it('creates an internal scroll viewport in fill-parent mode and keeps status outside it', () => { + const { container } = render( + + ); + + const viewport = container.querySelector('[data-slot="code-editor-viewport"]'); + + expect(viewport).toBeInTheDocument(); + expect(viewport).toHaveStyle({ height: '100%' }); + expect(viewport).not.toContainElement(screen.getByText('Valid JSON')); + }); + + it('keeps readonly status outside the scroll viewport for bounded editors', () => { + const { container } = render( + + ); + + const viewport = container.querySelector('[data-slot="code-editor-viewport"]'); + const textarea = container.querySelector('textarea'); + + expect(viewport).toHaveStyle({ height: 'calc(60vh - 120px)' }); + expect(textarea).toBeDisabled(); + expect(viewport).not.toContainElement(screen.getByText('(Read-only)')); + }); + + it('preserves content mode as the default layout contract', () => { + const { container } = render( + + ); + + expect(container.querySelector('[data-slot="code-editor-viewport"]')).not.toBeInTheDocument(); + }); +}); From aeab2840de0c69122c49dba1b6f286597e4d606a Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Mar 2026 15:31:24 -0400 Subject: [PATCH 3/4] fix(ui): constrain bounded code editor scroll viewports --- .../cliproxy/provider-editor/index.tsx | 6 ++-- ui/src/components/profiles/editor/index.tsx | 6 ++-- ui/src/components/shared/code-editor.tsx | 9 ++++-- ui/src/index.css | 32 +++++++++++++++++++ ui/src/pages/api.tsx | 2 +- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/ui/src/components/cliproxy/provider-editor/index.tsx b/ui/src/components/cliproxy/provider-editor/index.tsx index 502b8583..55d858f0 100644 --- a/ui/src/components/cliproxy/provider-editor/index.tsx +++ b/ui/src/components/cliproxy/provider-editor/index.tsx @@ -234,8 +234,8 @@ export function ProviderEditor({ Loading settings...
) : ( -
-
+
+
@@ -303,7 +303,7 @@ export function ProviderEditor({
-
+
diff --git a/ui/src/components/profiles/editor/index.tsx b/ui/src/components/profiles/editor/index.tsx index d13c4353..f9709845 100644 --- a/ui/src/components/profiles/editor/index.tsx +++ b/ui/src/components/profiles/editor/index.tsx @@ -226,8 +226,8 @@ export function ProfileEditor({
) : ( -
-
+
+
-
+
diff --git a/ui/src/components/shared/code-editor.tsx b/ui/src/components/shared/code-editor.tsx index e1dd049e..5dbc3fd4 100644 --- a/ui/src/components/shared/code-editor.tsx +++ b/ui/src/components/shared/code-editor.tsx @@ -156,12 +156,16 @@ export function CodeEditor({ ); return ( -
+
{/* Editor container */}
-
+
{selectedProfileData ? ( <>
From 92d067d0622c9a02c11b4b20b7416901a0929b4f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Thu, 26 Mar 2026 15:31:32 -0400 Subject: [PATCH 4/4] test(ui): cover bounded editor height contracts --- .../ui/code-editor-usage-contract.test.ts | 32 +++++++++++++++++++ .../unit/components/ui/code-editor.test.tsx | 6 ++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts b/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts index 7175b8d8..57c638c6 100644 --- a/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts +++ b/ui/tests/unit/components/ui/code-editor-usage-contract.test.ts @@ -29,6 +29,27 @@ const boundedConsumers = [ }, ] as const; +const boundedLayoutContracts = [ + { + file: 'src/components/profiles/editor/index.tsx', + snippets: [ + 'min-h-0 flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden', + 'flex min-h-0 min-w-0 flex-col overflow-hidden', + ], + }, + { + file: 'src/components/cliproxy/provider-editor/index.tsx', + snippets: [ + 'min-h-0 flex-1 grid grid-cols-[40%_60%] divide-x overflow-hidden', + 'flex min-h-0 min-w-0 flex-col overflow-hidden', + ], + }, + { + file: 'src/pages/api.tsx', + snippets: ['flex min-h-0 flex-1 flex-col min-w-0 overflow-hidden'], + }, +] as const; + describe('bounded CodeEditor consumers', () => { it.each(boundedConsumers)('$file opts into fill-parent mode for every bounded editor', ({ file, @@ -39,4 +60,15 @@ describe('bounded CodeEditor consumers', () => { expect(matches).toHaveLength(expectedCount); }); + + it.each(boundedLayoutContracts)( + '$file keeps bounded editor ancestors shrinkable', + ({ file, snippets }) => { + const source = readFileSync(resolve(process.cwd(), file), 'utf8'); + + for (const snippet of snippets) { + expect(source).toContain(snippet); + } + } + ); }); diff --git a/ui/tests/unit/components/ui/code-editor.test.tsx b/ui/tests/unit/components/ui/code-editor.test.tsx index 3a8d71cc..67e8b4c8 100644 --- a/ui/tests/unit/components/ui/code-editor.test.tsx +++ b/ui/tests/unit/components/ui/code-editor.test.tsx @@ -20,9 +20,10 @@ describe('CodeEditor', () => { ); const viewport = container.querySelector('[data-slot="code-editor-viewport"]'); + const root = container.firstElementChild; expect(viewport).toBeInTheDocument(); - expect(viewport).toHaveStyle({ height: '100%' }); + expect(root).toHaveStyle({ height: '100%' }); expect(viewport).not.toContainElement(screen.getByText('Valid JSON')); }); @@ -40,8 +41,9 @@ describe('CodeEditor', () => { const viewport = container.querySelector('[data-slot="code-editor-viewport"]'); const textarea = container.querySelector('textarea'); + const root = container.firstElementChild; - expect(viewport).toHaveStyle({ height: 'calc(60vh - 120px)' }); + expect(root).toHaveStyle({ height: 'calc(60vh - 120px)' }); expect(textarea).toBeDisabled(); expect(viewport).not.toContainElement(screen.getByText('(Read-only)')); });