From 2afec08c4f2ef6e7eed4716aaf4609586281635e Mon Sep 17 00:00:00 2001 From: menzelj Date: Mon, 31 Aug 2026 00:49:40 +0200 Subject: [PATCH] Fix "Add variable" doing nothing, and size the editors to the viewport (0.42.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The env table derived its rows from the serialized text on every render, and serialize() drops rows with an empty key — so a freshly added blank row was discarded before it could be typed into. The rows are now owned by the component and re-parsed only when `value` changes from outside, with stable per-row ids so deleting a row no longer shifts the reveal state onto its neighbour. AppShell's
is content-height, so the editor page's `h-full` collapsed to auto: Monaco and the raw .env textarea fell back to their intrinsic size, the textarea to a two-row default. The page is now sized against the viewport minus the top bar and page padding, so both editors fill the screen, and the textarea gets min-h-0 so flex-1 can grow it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016pMmFFkdfxkoYjcEcpZTa5 --- backend/version.py | 2 +- frontend/package.json | 2 +- frontend/src/components/env/EnvEditor.tsx | 192 +++++++++++++--------- frontend/src/pages/StackEditor.tsx | 6 +- 4 files changed, 119 insertions(+), 83 deletions(-) diff --git a/backend/version.py b/backend/version.py index 4de2d68..62ebe57 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,3 +1,3 @@ """Single source of truth for the StackPilot release version.""" -APP_VERSION = "0.42.1" +APP_VERSION = "0.42.2" diff --git a/frontend/package.json b/frontend/package.json index d0f8f1b..0c862f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.42.1", + "version": "0.42.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/env/EnvEditor.tsx b/frontend/src/components/env/EnvEditor.tsx index d63a8f9..c26dc84 100644 --- a/frontend/src/components/env/EnvEditor.tsx +++ b/frontend/src/components/env/EnvEditor.tsx @@ -1,30 +1,33 @@ -import { useMemo, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Plus, Trash2, Eye, EyeOff, Table, FileText } from "lucide-react"; import { Button, Input } from "@/components/ui"; interface Row { + /** Stable across edits, so deleting a row doesn't shift the reveal state + * (or input focus) onto its neighbour. */ + id: string; key: string; value: string; } const SENSITIVE_RE = /(PASS|SECRET|TOKEN|KEY|APIKEY|PWD|CREDENTIAL)/i; +let nextRowId = 0; +const newRow = (key = "", value = ""): Row => ({ id: `r${nextRowId++}`, key, value }); + function parseEnv(text: string): Row[] { return text .split("\n") .filter((l) => l.trim() && !l.trim().startsWith("#") && l.includes("=")) .map((l) => { const idx = l.indexOf("="); - return { key: l.slice(0, idx).trim(), value: l.slice(idx + 1) }; + return newRow(l.slice(0, idx).trim(), l.slice(idx + 1)); }); } function serialize(rows: Row[]): string { - return rows - .filter((r) => r.key.trim()) - .map((r) => `${r.key.trim()}=${r.value}`) - .join("\n") - .concat(rows.length ? "\n" : ""); + const named = rows.filter((r) => r.key.trim()); + return named.map((r) => `${r.key.trim()}=${r.value}`).join("\n") + (named.length ? "\n" : ""); } const QUICK = [ @@ -41,37 +44,49 @@ export function EnvEditor({ onChange: (v: string) => void; }) { const [mode, setMode] = useState<"table" | "raw">("table"); - const [reveal, setReveal] = useState>({}); - const rows = useMemo(() => parseEnv(value), [value]); + const [reveal, setReveal] = useState>({}); - const update = (next: Row[]) => onChange(serialize(next)); + // The rows are owned here rather than derived from `value` on every render: + // serialize() drops rows with an empty key, so a freshly added (still blank) + // row would be thrown away before it could ever be typed into — which is why + // "Add variable" appeared to do nothing. + const [rows, setRows] = useState(() => parseEnv(value)); + const lastEmitted = useRef(value); - const setRow = (i: number, patch: Partial) => - update(rows.map((r, idx) => (idx === i ? { ...r, ...patch } : r))); - const addRow = (row: Row = { key: "", value: "" }) => update([...rows, row]); - const delRow = (i: number) => update(rows.filter((_, idx) => idx !== i)); + useEffect(() => { + // Only re-parse when `value` changed somewhere else (loading a stack, + // editing in raw mode); echoes of our own edits must not clobber blank rows. + if (value !== lastEmitted.current) { + lastEmitted.current = value; + setRows(parseEnv(value)); + } + }, [value]); + + const update = (next: Row[]) => { + setRows(next); + const text = serialize(next); + lastEmitted.current = text; + onChange(text); + }; + + const setRow = (id: string, patch: Partial) => + update(rows.map((r) => (r.id === id ? { ...r, ...patch } : r))); + const addRow = (row?: { key: string; value: string }) => + update([...rows, row ? newRow(row.key, row.value) : newRow()]); + const delRow = (id: string) => update(rows.filter((r) => r.id !== id)); + + const tabClass = (active: boolean) => + active + ? "flex items-center gap-1 rounded bg-accent px-2 py-1 text-xs text-white dark:bg-accent-dark dark:text-slate-900" + : "flex items-center gap-1 rounded border border-slate-300 px-2 py-1 text-xs dark:border-slate-600"; return ( -
+
-
+ {/* Outside the scroll area: with a long list the button would + otherwise sit below the fold. */} +
diff --git a/frontend/src/pages/StackEditor.tsx b/frontend/src/pages/StackEditor.tsx index 029bd2c..ac7ef44 100644 --- a/frontend/src/pages/StackEditor.tsx +++ b/frontend/src/pages/StackEditor.tsx @@ -169,7 +169,11 @@ export function StackEditor() { }; return ( -
+ // AppShell's
is content-height, so `h-full` here would collapse to + // auto and leave the editors at their intrinsic (tiny) size. Size against + // the viewport instead, minus the top bar (pt-[76px]) and the page's + // bottom padding (pb-10), so the editor fills whatever screen the user has. +