From 82c2fe771f0febb07f992da3366defdf14f35802 Mon Sep 17 00:00:00 2001 From: Maximilian Scheurer <11406414+maxscheurer@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:33:35 +0200 Subject: [PATCH 01/15] chore: open PR for issue 396 From 3425a06c5ab6bc6481de1a2718f0fa14b29da499 Mon Sep 17 00:00:00 2001 From: Maximilian Scheurer <11406414+maxscheurer@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:09:54 +0200 Subject: [PATCH 02/15] Add ask_user cross-surface UX prototype --- packages/dashboard/package.json | 1 + .../test/client/ask-user.browser-harness.html | 12 + .../test/client/ask-user.browser-harness.tsx | 352 ++++++++++++++++++ .../test/client/ask-user.browser.test.ts | 142 +++++++ .../test/client/ask-user.vite.config.ts | 21 ++ 5 files changed, 528 insertions(+) create mode 100644 packages/dashboard/test/client/ask-user.browser-harness.html create mode 100644 packages/dashboard/test/client/ask-user.browser-harness.tsx create mode 100644 packages/dashboard/test/client/ask-user.browser.test.ts create mode 100644 packages/dashboard/test/client/ask-user.vite.config.ts diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 9f807e6d..9d5fe6b7 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -28,6 +28,7 @@ "clean": "shx rm -rf dist", "build": "tsgo -p tsconfig.build.json && vite build && shx chmod +x dist/index.js", "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput", + "prototype:ask-user": "vite --config test/client/ask-user.vite.config.ts", "profile:mobile": "node scripts/profile-mobile.mjs", "test": "vitest --run", "prepublishOnly": "npm run clean && npm run build" diff --git a/packages/dashboard/test/client/ask-user.browser-harness.html b/packages/dashboard/test/client/ask-user.browser-harness.html new file mode 100644 index 00000000..7bbfa33c --- /dev/null +++ b/packages/dashboard/test/client/ask-user.browser-harness.html @@ -0,0 +1,12 @@ + + + + + + ask_user UX prototype — dreb + + +
+ + + diff --git a/packages/dashboard/test/client/ask-user.browser-harness.tsx b/packages/dashboard/test/client/ask-user.browser-harness.tsx new file mode 100644 index 00000000..94327981 --- /dev/null +++ b/packages/dashboard/test/client/ask-user.browser-harness.tsx @@ -0,0 +1,352 @@ +import { createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js"; +import { render } from "solid-js/web"; + +type ScenarioId = "single" | "multiple" | "free-text" | "multiline" | "timeout" | "aborted"; +type Surface = "dashboard" | "tui"; + +interface Scenario { + id: ScenarioId; + label: string; + title: string; + question: string; + options: string[]; + multiSelect?: boolean; + multiline?: boolean; + terminal?: "timed out" | "cancelled"; +} + +const scenarios: Scenario[] = [ + { + id: "single", + label: "single choice", + title: "Choose an implementation", + question: "Which persistence strategy should I use?", + options: ["SQLite", "PostgreSQL", "Keep the current JSON file"], + }, + { + id: "multiple", + label: "multiple choice", + title: "Select validation targets", + question: "Which checks should run before each release?", + options: ["Unit tests", "Browser tests", "Type checking", "Workspace link verification"], + multiSelect: true, + }, + { + id: "free-text", + label: "free text", + title: "Name the new command", + question: "What should the user-facing command be called?", + options: [], + }, + { + id: "multiline", + label: "multiline", + title: "Describe the migration constraint", + question: "What compatibility requirements must the migration preserve?", + options: [], + multiline: true, + }, + { + id: "timeout", + label: "timeout", + title: "Choose a deployment window", + question: "When may I restart the production service?", + options: ["Now", "After 18:00 UTC"], + terminal: "timed out", + }, + { + id: "aborted", + label: "abort", + title: "Confirm the target", + question: "Which environment should receive this change?", + options: ["Staging", "Production"], + terminal: "cancelled", + }, +]; + +function answerSummary(selected: string[], custom: string): string { + return [...selected, custom.trim()].filter(Boolean).join("; "); +} + +function AskPanel(props: { scenario: Scenario; surface: Surface }): JSX.Element { + const [selected, setSelected] = createSignal([]); + const [custom, setCustom] = createSignal(""); + const [outcome, setOutcome] = createSignal<"answered" | "skipped" | undefined>(); + const terminal = () => props.scenario.terminal; + const disabled = () => terminal() !== undefined || outcome() !== undefined; + const inputId = () => `${props.surface}-${props.scenario.id}-custom`; + const groupName = () => `${props.surface}-${props.scenario.id}-options`; + + createEffect(() => { + props.scenario.id; + setSelected([]); + setCustom(""); + setOutcome(undefined); + }); + + const toggle = (option: string) => { + if (disabled()) return; + if (!props.scenario.multiSelect) { + setSelected([option]); + return; + } + setSelected((current) => + current.includes(option) ? current.filter((value) => value !== option) : [...current, option], + ); + }; + const submit = () => { + if (!disabled() && answerSummary(selected(), custom())) setOutcome("answered"); + }; + + return ( +
+
+ {props.surface === "dashboard" ? "Dashboard" : "TUI"} + ◆ needs attention +
+
+
+ + {props.surface === "tui" ? "? " : ""} + {props.scenario.title} + + + {terminal() === "timed out" + ? "timed out" + : terminal() === "cancelled" + ? "cancelled" + : "01:42 remaining"} + +
+

{props.scenario.question}

+ + }> + 0}> +
+ {props.scenario.multiSelect ? "Choose one or more" : "Choose one"} + + {(option) => ( + + )} + +
+
+ +
+ + setCustom(event.currentTarget.value)} + /> + } + > +