mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-23 16:13:35 +00:00
feat(tui): experiments via devtools bar, drafts stay put (#41917)
This commit is contained in:
parent
07bcd290c2
commit
1b45061afb
6 changed files with 35 additions and 40 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(() => <DialogExperiments />)
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
name: "opencode.status",
|
||||
title: "View status",
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</PanelBox>
|
||||
</Show>
|
||||
</BarItem>
|
||||
<BarItem
|
||||
active={false}
|
||||
onClick={() => {
|
||||
close()
|
||||
dialog.replace(() => <DialogExperiments />)
|
||||
}}
|
||||
>
|
||||
<text fg={theme.text.subdued}>Experiments</text>
|
||||
</BarItem>
|
||||
<box flexGrow={1} minWidth={0}>
|
||||
<TimeToFirstDraw visible={timing()} width="100%" fg={theme.text.subdued} label="Time to first draw" />
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<DialogSelect
|
||||
title="Experiments"
|
||||
options={options()}
|
||||
onSelect={(option) => 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(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ declare module "@opentui/keymap" {
|
|||
name: string
|
||||
aliases?: string[]
|
||||
arguments?: true
|
||||
secret?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue