feat(tui): group sequential thinking (#36901)
Co-authored-by: Kit Langton <7587245+kitlangton@users.noreply.github.com>
This commit is contained in:
parent
b67bed061a
commit
016545513f
8 changed files with 262 additions and 99 deletions
|
|
@ -1,7 +1,6 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { TextareaRenderable } from "@opentui/core"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
|
@ -27,31 +26,26 @@ async function mountPrompt(input: {
|
|||
const state = path.join(input.root, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
|
||||
const [
|
||||
{ DialogProvider },
|
||||
{ DialogPrompt },
|
||||
{ ThemeProvider },
|
||||
{ ConfigProvider },
|
||||
{ ToastProvider },
|
||||
{ OpencodeKeymapProvider, registerOpencodeKeymap },
|
||||
] = await Promise.all([
|
||||
import("../../../src/ui/dialog"),
|
||||
import("../../../src/ui/dialog-prompt"),
|
||||
import("../../../src/context/theme"),
|
||||
import("../../../src/config"),
|
||||
import("../../../src/ui/toast"),
|
||||
import("../../../src/keymap"),
|
||||
])
|
||||
const [{ DialogProvider }, { DialogPrompt }, { ThemeProvider }, { ConfigProvider }, { ToastProvider }, { Keymap }] =
|
||||
await Promise.all([
|
||||
import("../../../src/ui/dialog"),
|
||||
import("../../../src/ui/dialog-prompt"),
|
||||
import("../../../src/context/theme"),
|
||||
import("../../../src/config"),
|
||||
import("../../../src/ui/toast"),
|
||||
import("../../../src/context/keymap"),
|
||||
])
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const resolvedConfig = createTuiResolvedConfig({
|
||||
keybinds: input.keybinds,
|
||||
leader: { timeout: 1000 },
|
||||
})
|
||||
const off = registerOpencodeKeymap(keymap, renderer, resolvedConfig)
|
||||
onCleanup(off)
|
||||
|
||||
function Prompt() {
|
||||
onCleanup(Keymap.use().mode.push("modal"))
|
||||
return <DialogPrompt title="Rename Session" value="draft" onConfirm={input.onConfirm} />
|
||||
}
|
||||
|
||||
return (
|
||||
<TestTuiContexts
|
||||
|
|
@ -62,22 +56,23 @@ async function mountPrompt(input: {
|
|||
worktree: input.root,
|
||||
}}
|
||||
>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<ConfigProvider config={resolvedConfig}>
|
||||
<ConfigProvider config={resolvedConfig}>
|
||||
<Keymap.Provider>
|
||||
<ThemeProvider mode="dark">
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<DialogPrompt title="Rename Session" value="draft" onConfirm={input.onConfirm} />
|
||||
<Prompt />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
||||
const app = await testRender(() => <Harness />, { kittyKeyboard: true })
|
||||
app.renderer.start()
|
||||
return {
|
||||
app,
|
||||
async cleanup() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { InputRenderable } from "@opentui/core"
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
|
@ -21,58 +20,54 @@ async function renderSelect(
|
|||
const state = path.join(root, "state")
|
||||
await mkdir(state, { recursive: true })
|
||||
const config = createTuiResolvedConfig()
|
||||
const [
|
||||
{ ConfigProvider },
|
||||
{ ThemeProvider },
|
||||
{ OpencodeKeymapProvider, registerOpencodeKeymap },
|
||||
{ DialogProvider },
|
||||
{ DialogSelect },
|
||||
{ ToastProvider },
|
||||
] = await Promise.all([
|
||||
import("../../../src/config"),
|
||||
import("../../../src/context/theme"),
|
||||
import("../../../src/keymap"),
|
||||
import("../../../src/ui/dialog"),
|
||||
import("../../../src/ui/dialog-select"),
|
||||
import("../../../src/ui/toast"),
|
||||
])
|
||||
const [{ ConfigProvider }, { ThemeProvider }, { Keymap }, { DialogProvider }, { DialogSelect }, { ToastProvider }] =
|
||||
await Promise.all([
|
||||
import("../../../src/config"),
|
||||
import("../../../src/context/theme"),
|
||||
import("../../../src/context/keymap"),
|
||||
import("../../../src/ui/dialog"),
|
||||
import("../../../src/ui/dialog-select"),
|
||||
import("../../../src/ui/toast"),
|
||||
])
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const off = registerOpencodeKeymap(keymap, renderer, config)
|
||||
onCleanup(off)
|
||||
function Select() {
|
||||
onCleanup(Keymap.use().mode.push("modal"))
|
||||
return (
|
||||
<DialogSelect
|
||||
title="Items"
|
||||
options={options}
|
||||
current={current}
|
||||
actions={[
|
||||
{
|
||||
command: "dialog.move_session.delete",
|
||||
title: "delete",
|
||||
onTrigger: onRow,
|
||||
},
|
||||
{
|
||||
command: "dialog.move_session.new",
|
||||
title: "new",
|
||||
selection: "none",
|
||||
onTrigger: onGlobal,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TestTuiContexts directory={root} paths={{ home: root, state, worktree: root }}>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<ConfigProvider config={config}>
|
||||
<ConfigProvider config={config}>
|
||||
<Keymap.Provider>
|
||||
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
<DialogSelect
|
||||
title="Items"
|
||||
options={options}
|
||||
current={current}
|
||||
actions={[
|
||||
{
|
||||
command: "dialog.move_session.delete",
|
||||
title: "delete",
|
||||
onTrigger: onRow,
|
||||
},
|
||||
{
|
||||
command: "dialog.move_session.new",
|
||||
title: "new",
|
||||
selection: "none",
|
||||
onTrigger: onGlobal,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Select />
|
||||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
|
@ -91,14 +86,14 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[])
|
|||
const [
|
||||
{ ConfigProvider },
|
||||
{ ThemeProvider },
|
||||
{ OpencodeKeymapProvider, registerOpencodeKeymap },
|
||||
{ Keymap },
|
||||
{ DialogProvider, useDialog },
|
||||
{ DialogSelect },
|
||||
{ ToastProvider },
|
||||
] = await Promise.all([
|
||||
import("../../../src/config"),
|
||||
import("../../../src/context/theme"),
|
||||
import("../../../src/keymap"),
|
||||
import("../../../src/context/keymap"),
|
||||
import("../../../src/ui/dialog"),
|
||||
import("../../../src/ui/dialog-select"),
|
||||
import("../../../src/ui/toast"),
|
||||
|
|
@ -109,12 +104,8 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[])
|
|||
let replaceOptions!: (options: DialogSelectOption<string>[]) => void
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const off = registerOpencodeKeymap(keymap, renderer, config)
|
||||
const [options, setOptions] = createSignal(initial)
|
||||
replaceOptions = setOptions
|
||||
onCleanup(off)
|
||||
|
||||
function Fixture() {
|
||||
const dialog = useDialog()
|
||||
|
|
@ -133,8 +124,8 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[])
|
|||
|
||||
return (
|
||||
<TestTuiContexts directory={root} paths={{ home: root, state, worktree: root }}>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<ConfigProvider config={config}>
|
||||
<ConfigProvider config={config}>
|
||||
<Keymap.Provider>
|
||||
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
|
||||
<ToastProvider>
|
||||
<DialogProvider>
|
||||
|
|
@ -142,8 +133,8 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[])
|
|||
</DialogProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</ConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
|
||||
import { testRender, useRenderer } from "@opentui/solid"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { onCleanup } from "solid-js"
|
||||
import { ClipboardProvider } from "../../../src/context/clipboard"
|
||||
import type { FormWithLocation } from "../../../src/context/data"
|
||||
import { ClientProvider } from "../../../src/context/client"
|
||||
import { ThemeProvider } from "../../../src/context/theme"
|
||||
import { Keymap } from "../../../src/context/keymap"
|
||||
import { ConfigProvider } from "../../../src/config"
|
||||
import { OpencodeKeymapProvider, registerOpencodeKeymap } from "../../../src/keymap"
|
||||
import { ToastProvider } from "../../../src/ui/toast"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||
|
|
@ -51,11 +49,6 @@ async function mountForm(root: string, width = 80) {
|
|||
const { FormPrompt } = await import("../../../src/routes/session/form")
|
||||
|
||||
function Harness() {
|
||||
const renderer = useRenderer()
|
||||
const keymap = createDefaultOpenTuiKeymap(renderer)
|
||||
const off = registerOpencodeKeymap(keymap, renderer, config)
|
||||
onCleanup(off)
|
||||
|
||||
return (
|
||||
<TestTuiContexts
|
||||
directory={root}
|
||||
|
|
@ -73,8 +66,8 @@ async function mountForm(root: string, width = 80) {
|
|||
},
|
||||
}}
|
||||
>
|
||||
<OpencodeKeymapProvider keymap={keymap}>
|
||||
<ConfigProvider config={config}>
|
||||
<ConfigProvider config={config}>
|
||||
<Keymap.Provider>
|
||||
<ClientProvider api={createApi(transport.fetch)}>
|
||||
<ThemeProvider mode="dark" source={{ discover: () => Promise.resolve({}) }}>
|
||||
<ToastProvider>
|
||||
|
|
@ -82,8 +75,8 @@ async function mountForm(root: string, width = 80) {
|
|||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
</ClientProvider>
|
||||
</ConfigProvider>
|
||||
</OpencodeKeymapProvider>
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
</ClipboardProvider>
|
||||
</TestTuiContexts>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -74,9 +74,49 @@ test("assigns stable kind ordinals within an assistant message", () => {
|
|||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "reasoning:0" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:1" } },
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "reasoning:1" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:1" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("groups adjacent reasoning parts until a visible boundary", () => {
|
||||
const messages: SessionMessageInfo[] = [
|
||||
assistant("assistant-1", [
|
||||
{ type: "reasoning", text: "First" },
|
||||
{ type: "reasoning", text: "Second" },
|
||||
{ type: "text", text: "Visible" },
|
||||
{ type: "reasoning", text: "Third" },
|
||||
]),
|
||||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [
|
||||
{ messageID: "assistant-1", partID: "reasoning:0" },
|
||||
{ messageID: "assistant-1", partID: "reasoning:1" },
|
||||
],
|
||||
},
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "text:0" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: false,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:2" }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
|
|
@ -93,7 +133,12 @@ test("groups across empty assistant reasoning parts", () => {
|
|||
]
|
||||
|
||||
expect(reduceSessionRows(messages)).toEqual([
|
||||
{ type: "part", ref: { messageID: "assistant-1", partID: "reasoning:0" } },
|
||||
{
|
||||
type: "group",
|
||||
kind: "reasoning",
|
||||
completed: true,
|
||||
refs: [{ messageID: "assistant-1", partID: "reasoning:0" }],
|
||||
},
|
||||
{
|
||||
type: "group",
|
||||
kind: "exploration",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue