From 1b45061afbe8938589d3712fe50d170f2c6de2db Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 11 Aug 2026 22:08:06 -0400 Subject: [PATCH] feat(tui): experiments via devtools bar, drafts stay put (#41917) --- packages/plugin/src/tui/context.ts | 2 -- packages/tui/src/app.tsx | 30 +------------------ packages/tui/src/component/devtools-bar.tsx | 12 ++++++++ .../tui/src/component/dialog-experiments.tsx | 27 +++++++++++++---- .../tui/src/component/prompt/autocomplete.tsx | 3 -- packages/tui/src/context/keymap.tsx | 1 - 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts index 616b266a28d..e1ba963131d 100644 --- a/packages/plugin/src/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -372,8 +372,6 @@ export interface KeymapCommand { readonly aliases?: string[] /** Keeps the slash command in the prompt and passes its raw input to run. */ readonly arguments?: true - /** Hides the command from slash completion until its exact name is typed. */ - readonly secret?: true } /** Promotes the command in discovery UI. */ readonly suggested?: boolean | (() => boolean) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index dbf2a6299b8..377ebb598be 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -30,7 +30,7 @@ import { batch, Show, } from "solid-js" -import { createStore, unwrap } from "solid-js/store" +import { createStore } from "solid-js/store" import { TuiLifecycleProvider, TuiAppProvider, @@ -62,7 +62,6 @@ import { useConnected } from "./component/use-connected" import { DialogMcp } from "./component/dialog-mcp" import { DialogStatus } from "./component/dialog-status" import { DialogConfig } from "./component/dialog-config" -import { DialogExperiments } from "./component/dialog-experiments" import { DialogDebug } from "./component/dialog-debug" import { DialogPair, type DialogPairCredentials } from "./component/dialog-pair" import { DialogThemeList } from "./component/dialog-theme-list" @@ -658,22 +657,8 @@ function App(props: { pair?: DialogPairCredentials }) { category: "Session", slash: { name: "new", aliases: ["clear"] }, run: () => { - // With per-tab drafts, a new session is an explicit "this belongs - // elsewhere" gesture: move the in-progress draft instead of leaving - // a copy behind on the tab it came from. - const carried = (() => { - if (config.data.experimental?.tab_drafts !== true) return undefined - const current = promptRef.current - if (!current?.current.text) return undefined - // Copy before reset: reset() merges an empty prompt into the same - // underlying store object that unwrap exposes. - const prompt = { ...unwrap(current.current) } - current.reset() - return prompt - })() route.navigate({ type: "home", - prompt: carried, location: route.data.type === "session" ? (data.session.get(route.data.sessionID)?.location ?? location.ref) @@ -885,19 +870,6 @@ function App(props: { pair?: DialogPairCredentials }) { }, category: "System", }, - { - // Deliberately absent from the command palette; reachable only by the - // secret /baldbeard incantation. - name: "opencode.experiments", - title: "Experiments", - description: "look is my devrel meme face", - palette: undefined, - slash: { name: "baldbeard", secret: true as const }, - run: () => { - dialog.replace(() => ) - }, - category: "System", - }, { name: "opencode.status", title: "View status", diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index f6c387971e5..3b1fbe76ab5 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -13,6 +13,8 @@ import { useRoute } from "../context/route" import { Keymap } from "../context/keymap" import { useTheme, useThemes } from "../context/theme" import { DevTools } from "../devtools" +import { useDialog } from "../ui/dialog" +import { DialogExperiments } from "./dialog-experiments" import { usePlugin } from "../plugin/context" import { errorMessage } from "../util/error" @@ -27,6 +29,7 @@ export type RuntimeStatus = "normal" | "medium" | "high" export function DevToolsBar() { const client = useClient() const config = useConfig() + const dialog = useDialog() const data = useData() const location = useLocation() const route = useRoute() @@ -405,6 +408,15 @@ export function DevToolsBar() { + { + close() + dialog.replace(() => ) + }} + > + Experiments + diff --git a/packages/tui/src/component/dialog-experiments.tsx b/packages/tui/src/component/dialog-experiments.tsx index dda144515f9..9db2a623618 100644 --- a/packages/tui/src/component/dialog-experiments.tsx +++ b/packages/tui/src/component/dialog-experiments.tsx @@ -16,13 +16,14 @@ export const experiments: Experiment[] = [ { id: "tab_drafts", title: "Per-tab prompt drafts", - description: "Keep unsent prompt drafts on the tab where they were written. New session moves the current draft.", + description: "Keep unsent prompt drafts on the tab where they were written. New sessions start blank.", }, ] export function DialogExperiments() { const config = useConfig() const toast = useToast() + const [selected, setSelected] = createSignal(0) const [saving, setSaving] = createSignal(false) const enabled = (experiment: Experiment) => config.data.experimental?.[experiment.id] === true @@ -30,14 +31,15 @@ export function DialogExperiments() { const options = createMemo(() => experiments.map((experiment, index) => ({ title: experiment.title, - description: experiment.description, category: "Experiments", + searchText: experiment.description, footer: enabled(experiment) ? "on" : "off", value: index, })), ) - async function toggle(index: number) { + // All experiments are booleans, so either direction toggles. + async function change(index = selected()) { if (saving()) return const experiment = experiments[index] if (!experiment) return @@ -56,8 +58,23 @@ export function DialogExperiments() { void toggle(option.value)} - footerHints={[{ title: "enter", label: "toggle" }]} + onMove={(option) => setSelected(option.value)} + onSelect={(option) => void change(option.value)} + footerHints={[{ title: "←/→", label: "change" }]} + bindings={[ + { + bind: "left", + title: "Previous value", + group: "Experiments", + run: () => void change(), + }, + { + bind: "right", + title: "Next value", + group: "Experiments", + run: () => void change(), + }, + ]} /> ) } diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index e018fec3bcb..4600491f90c 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -512,9 +512,6 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = keymapCommands().flatMap((command) => { const slash = command.slash if (!slash) return [] - // Secret commands are incantations: absent from the "/" listing and from - // fuzzy matching until the exact name is typed. - if (slash.secret && search().toLowerCase() !== slash.name) return [] return { display: `/${slash.name}`, description: command.description ?? command.title, diff --git a/packages/tui/src/context/keymap.tsx b/packages/tui/src/context/keymap.tsx index 0cfa8737dcd..a7a28efa127 100644 --- a/packages/tui/src/context/keymap.tsx +++ b/packages/tui/src/context/keymap.tsx @@ -24,7 +24,6 @@ declare module "@opentui/keymap" { name: string aliases?: string[] arguments?: true - secret?: true } } }