Fix new secret form textarea overflow (PAPA-348) (#6222)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Operators manage per-company secrets through the Secrets page in the web UI > - A long secret value pasted into the "New secret" textarea blew out the form's width, which pushed the Create/Cancel buttons off-screen and made the form unusable > - Root cause: the shadcn `Textarea` primitive sets `w-full` but does not constrain `min-width`, so a flex parent honors the textarea's intrinsic content width when a long unbreakable string is present > - This pull request adds `min-w-0 max-w-full` to the shared `Textarea` primitive and `min-w-0 overflow-x-hidden break-all` on the secret-value usage so a long token wraps inside the form bounds > - The benefit is the Create/Cancel buttons stay reachable regardless of pasted token length, and every other `Textarea` consumer also gets the flexbox-friendly width constraint ## What Changed - `ui/src/components/ui/textarea.tsx`: added `min-w-0 max-w-full` to the base shadcn `Textarea` so it cannot exceed its flex parent - `ui/src/pages/Secrets.tsx`: added `min-w-0 overflow-x-hidden break-all` on the new-secret value `Textarea` so long opaque tokens wrap instead of pushing the form - `ui/src/pages/Secrets.render.test.tsx`: new regression test that opens the New Secret dialog and asserts the value textarea carries the width-constraint classes ## Verification - `cd ui && npx vitest run src/pages/Secrets.render.test.tsx` — 3/3 pass - Manual: open the Secrets page, click "New secret", paste a long unbroken string (e.g. a 500-char token) into the value field. The form stays within its dialog and the Create/Cancel buttons remain in view. Before: <img width="1772" height="1432" alt="image" src="https://github.com/user-attachments/assets/cb31a290-f82a-41dc-9346-91d18cbb5911" /> After: <img width="672" height="734" alt="Screenshot 2026-05-17 at 5 39 38 PM" src="https://github.com/user-attachments/assets/a08800c2-b09b-43be-b0e8-114d9149b8f5" /> After: the value field wraps with `break-all` inside the dialog; Create/Cancel stay clickable. Covered by the new render test which asserts `min-w-0`, `overflow-x-hidden`, and `break-all` are present on `#new-secret-value`. ## Risks - Low risk. The base `Textarea` change adds `min-w-0 max-w-full`, which only affects layouts where a textarea was previously allowed to grow past its parent — those cases were already buggy. `break-all` on the secret-value textarea is the right behavior for opaque tokens; it would be wrong for prose, but this field is explicitly a secret token. ## Model Used - Provider: Anthropic Claude - Model: claude-opus-4-7 (Opus 4.7) - Mode: standard Claude Code agent, tool use enabled ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 min-w-0 w-full max-w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -305,4 +305,43 @@ describe("Secrets page layout", () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the new secret value textarea width-constrained for long tokens", async () => {
|
||||
const root = createRoot(container);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Secrets />
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
});
|
||||
await flushReact();
|
||||
await flushReact();
|
||||
|
||||
const newSecretButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent?.includes("New secret"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(newSecretButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
newSecretButton?.click();
|
||||
});
|
||||
await flushReact();
|
||||
|
||||
const secretValueTextarea = document.body.querySelector("#new-secret-value") as HTMLTextAreaElement | null;
|
||||
expect(secretValueTextarea).not.toBeNull();
|
||||
expect(secretValueTextarea?.className).toContain("min-w-0");
|
||||
expect(secretValueTextarea?.className).toContain("overflow-x-hidden");
|
||||
expect(secretValueTextarea?.className).toContain("break-all");
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1224,7 +1224,7 @@ export function Secrets() {
|
||||
setCreateForm((current) => ({ ...current, value: event.target.value }))
|
||||
}
|
||||
rows={3}
|
||||
className="font-mono text-xs"
|
||||
className="min-w-0 overflow-x-hidden break-all font-mono text-xs"
|
||||
placeholder="Stored once, never re-displayed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user