From bfac148aa6db360f4a96375ac9909de9fe40a888 Mon Sep 17 00:00:00 2001 From: Ellis Green Date: Mon, 17 Aug 2026 15:32:30 +0100 Subject: [PATCH 01/12] Fix the duplicate status pill and the note dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collapsed header drew its own outline Badge alongside StageRail's active pill. Deleting the Badge outright would have left nothing between sm and md, where the rail's
    is hidden, so the rail now renders at every width and hides its non-active steps below md instead. One component owns the status indicator and it cannot duplicate itself. NoteDialog never reset, and its only reset was guarded on `content`, which is undefined when creating — so a new note opened holding the last one's text. Reset on open, the way ColumnDialog already does. The field also accepted 255 characters in a 36px single-line Input. AutoTextarea grows to its cap instead, the dialog widens to hold it, and Cmd/Ctrl+Enter saves now that Enter inserts a newline. TaskDialog had the same reset bug and gets the same fix. ConnectionIndicator moves into the always-visible row: it sat inside CollapsibleContent, which Radix unmounts, so collapsing the header made presence disappear. Co-Authored-By: Claude Opus 5 --- .claude/launch.json | 11 +++++++++ ui/src/components/retro/board.tsx | 19 ++++----------- ui/src/components/retro/note-dialog.tsx | 31 ++++++++++++++++++------- ui/src/components/retro/stage-rail.tsx | 12 +++++++--- ui/src/components/retro/task-dialog.tsx | 24 +++++++++++-------- 5 files changed, 61 insertions(+), 36 deletions(-) create mode 100644 .claude/launch.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..513c6f6 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ui", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--dir", "ui", "run", "dev"], + "port": 5173 + } + ] +} diff --git a/ui/src/components/retro/board.tsx b/ui/src/components/retro/board.tsx index 9b07614..6e5d62f 100644 --- a/ui/src/components/retro/board.tsx +++ b/ui/src/components/retro/board.tsx @@ -13,7 +13,6 @@ import { } from "@/events"; import useRetro from "@/hooks/use-retro"; import { panelVariants } from "@/lib/motion"; -import { stageLabel } from "@/lib/stages"; import { RetroStatus } from "@/types"; import { Link } from "@tanstack/react-router"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; @@ -78,17 +77,13 @@ export default function Board() {
    - {!expanded && ( - - {stageLabel(status)} - - )} - + +
    diff --git a/ui/src/components/retro/note-dialog.tsx b/ui/src/components/retro/note-dialog.tsx index b222e20..823dc3d 100644 --- a/ui/src/components/retro/note-dialog.tsx +++ b/ui/src/components/retro/note-dialog.tsx @@ -16,9 +16,9 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +import { AutoTextarea } from "@/components/ui/auto-textarea"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; import { z } from "zod"; @@ -53,16 +53,20 @@ export default function NoteDialog({ onContentSave(data.content); }; - useEffect(() => { - if (content) { - form.reset({ content }); + function handleOpenChange(next: boolean) { + setOpen(next); + + // Reopening should show what the note says now, not the last thing typed + // into this dialog or someone else's live edit. + if (next) { + form.reset({ content: content ?? "" }); } - }, [content, form]); + } return ( - + {children} - + {title} {description} @@ -78,10 +82,19 @@ export default function NoteDialog({ Note - { + if ( + event.key === "Enter" && + (event.metaKey || event.ctrlKey) + ) { + event.currentTarget.form?.requestSubmit(); + } + }} {...field} /> diff --git a/ui/src/components/retro/stage-rail.tsx b/ui/src/components/retro/stage-rail.tsx index c26fc9c..ba32a6c 100644 --- a/ui/src/components/retro/stage-rail.tsx +++ b/ui/src/components/retro/stage-rail.tsx @@ -40,19 +40,25 @@ export default function StageRail({ return ( <>
    -
      +
        {stages.map((stage, index) => { const done = index < current; const active = index === current; const Icon = done ? Check : stage.icon; return ( -
      1. +
      2. {index > 0 && (
      3. ))} + +
        + + add + + + + + + + choose + +
        ); diff --git a/ui/src/components/ui/kbd.tsx b/ui/src/components/ui/kbd.tsx new file mode 100644 index 0000000..ff93b53 --- /dev/null +++ b/ui/src/components/ui/kbd.tsx @@ -0,0 +1,26 @@ +import { cn } from "@/lib/utils" + +function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { + return ( + + ) +} + +function KbdGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { Kbd, KbdGroup } diff --git a/ui/src/lib/keys.ts b/ui/src/lib/keys.ts new file mode 100644 index 0000000..d4c021d --- /dev/null +++ b/ui/src/lib/keys.ts @@ -0,0 +1,6 @@ +// navigator.platform is deprecated and userAgentData is Chromium-only, so this +// sniffs the UA. Getting it wrong only mislabels a hint — both modifiers are +// accepted wherever one is. +const isApple = /mac|iphone|ipad|ipod/i.test(navigator.userAgent); + +export const modKey = isApple ? "⌘" : "Ctrl"; From 8da98e9fb41231ae4a7b56ed1c9a75ff23b96799 Mon Sep 17 00:00:00 2001 From: Ellis Green Date: Tue, 18 Aug 2026 09:03:16 +0100 Subject: [PATCH 11/12] Refresh a retro's cached state when the socket changes it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaving a retro and coming back showed the stage you left rather than the one it is on, until a hard refresh. The route loader's result is cached by the router and survives navigating away. While the board is open the socket, not the loader, is what moves the retro forward, so the cached copy silently goes out of date. Coming back mounts the board and seeds it from that copy, and the seed only happens once, so a later revalidation never reaches the screen. Invalidating on retro_updated and status_updated keeps the cache honest. While the board is open this costs a loader re-run whose result is discarded — the socket has already applied it — so nothing on screen moves, and stage changes still keep their notes with no skeleton. Predates the work on this branch: verified by running main against the same backend, where advancing a stage and returning shows the old stage in exactly the same way. Co-Authored-By: Claude Opus 5 --- ui/src/routes/_auth.retros.$retroId.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/src/routes/_auth.retros.$retroId.tsx b/ui/src/routes/_auth.retros.$retroId.tsx index f82aa59..30c04c5 100644 --- a/ui/src/routes/_auth.retros.$retroId.tsx +++ b/ui/src/routes/_auth.retros.$retroId.tsx @@ -9,7 +9,7 @@ import { useSocketEvent } from "@/hooks/use-retro-socket"; import { api } from "@/lib/api"; import { SocketEvent } from "@/events"; import { Note, Retro } from "@/types"; -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, useRouter } from "@tanstack/react-router"; import { useMemo, useState } from "react"; export const Route = createFileRoute("/_auth/retros/$retroId")({ @@ -71,6 +71,7 @@ function RetroProvider({ loaded: Retro; children: React.ReactNode; }) { + const router = useRouter(); const [retro, setRetro] = useState(loaded); // Owned here rather than in Settings: that component lives inside the @@ -80,6 +81,14 @@ function RetroProvider({ if (event.name === "retro_updated") { setRetro(event.payload as Retro); } + + // Anything the socket reports leaves the loader's cached copy wrong, and + // the router keeps that copy after you navigate away. Coming back seeds + // the board from it, so without this you get the stage you left rather + // than the one the retro is on. + if (event.name === "retro_updated" || event.name === "status_updated") { + router.invalidate(); + } }); const value = useMemo(() => ({ retro, setRetro }), [retro]); From 9cdc20118d5b86842e4f1f63a93cc097f2ab1ea9 Mon Sep 17 00:00:00 2001 From: Ellis Green Date: Tue, 18 Aug 2026 09:14:02 +0100 Subject: [PATCH 12/12] Save a note on Enter instead of Cmd/Ctrl+Enter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shift+Enter still inserts a newline; plain Enter now submits, matching the chat-input convention most people already expect there. The Cmd/Ctrl+Enter path is gone rather than kept as an alt, so there's only one thing to learn. lib/keys.ts (the platform-aware ⌘/Ctrl label) was added for this hint and has no other caller, so it goes with it rather than sitting unused. Co-Authored-By: Claude Sonnet 5 --- ui/src/components/retro/note-dialog.tsx | 27 ++++++++++--------------- ui/src/lib/keys.ts | 6 ------ 2 files changed, 11 insertions(+), 22 deletions(-) delete mode 100644 ui/src/lib/keys.ts diff --git a/ui/src/components/retro/note-dialog.tsx b/ui/src/components/retro/note-dialog.tsx index 0b1bb6c..fcd1006 100644 --- a/ui/src/components/retro/note-dialog.tsx +++ b/ui/src/components/retro/note-dialog.tsx @@ -17,8 +17,7 @@ import { FormMessage, } from "@/components/ui/form"; import { AutoTextarea } from "@/components/ui/auto-textarea"; -import { Kbd, KbdGroup } from "@/components/ui/kbd"; -import { modKey } from "@/lib/keys"; +import { Kbd } from "@/components/ui/kbd"; import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { FieldValues, SubmitHandler, useForm } from "react-hook-form"; @@ -90,12 +89,13 @@ export default function NoteDialog({ autoFocus maxLength={255} onKeyDown={(event) => { - if ( - event.key === "Enter" && - (event.metaKey || event.ctrlKey) - ) { - event.currentTarget.form?.requestSubmit(); - } + if (event.key !== "Enter" || event.shiftKey) return; + + // Shift+Enter still makes a newline; plain Enter + // would otherwise just add one via the textarea's + // own default. + event.preventDefault(); + event.currentTarget.form?.requestSubmit(); }} {...field} /> @@ -109,14 +109,9 @@ export default function NoteDialog({ diff --git a/ui/src/lib/keys.ts b/ui/src/lib/keys.ts deleted file mode 100644 index d4c021d..0000000 --- a/ui/src/lib/keys.ts +++ /dev/null @@ -1,6 +0,0 @@ -// navigator.platform is deprecated and userAgentData is Chromium-only, so this -// sniffs the UA. Getting it wrong only mislabels a hint — both modifiers are -// accepted wherever one is. -const isApple = /mac|iphone|ipad|ipod/i.test(navigator.userAgent); - -export const modKey = isApple ? "⌘" : "Ctrl";