feat(tui): revamp session export (#35971)
This commit is contained in:
parent
19f42f7102
commit
79415f625a
3 changed files with 236 additions and 161 deletions
|
|
@ -1,38 +1,63 @@
|
|||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { onMount, Show } from "solid-js"
|
||||
import { For, Show } from "solid-js"
|
||||
import { useBindings } from "../keymap"
|
||||
|
||||
export type ExportFormat = "markdown" | "json"
|
||||
|
||||
export type DialogExportOptionsProps = {
|
||||
defaultFilename: string
|
||||
defaultThinking: boolean
|
||||
defaultToolDetails: boolean
|
||||
defaultAssistantMetadata: boolean
|
||||
defaultOpenWithoutSaving: boolean
|
||||
onConfirm?: (options: {
|
||||
filename: string
|
||||
action: "copy" | "export"
|
||||
format: ExportFormat
|
||||
debug: boolean
|
||||
thinking: boolean
|
||||
toolDetails: boolean
|
||||
assistantMetadata: boolean
|
||||
openWithoutSaving: boolean
|
||||
}) => void
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
type Active = ExportFormat | "debug" | "thinking" | "toolDetails" | "assistantMetadata" | "copy" | "export"
|
||||
|
||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
let textarea: TextareaRenderable
|
||||
const [store, setStore] = createStore({
|
||||
format: "markdown" as ExportFormat,
|
||||
debug: false,
|
||||
thinking: props.defaultThinking,
|
||||
toolDetails: props.defaultToolDetails,
|
||||
assistantMetadata: props.defaultAssistantMetadata,
|
||||
openWithoutSaving: props.defaultOpenWithoutSaving,
|
||||
active: "filename" as "filename" | "thinking" | "toolDetails" | "assistantMetadata" | "openWithoutSaving",
|
||||
active: "markdown" as Active,
|
||||
})
|
||||
|
||||
const confirm = (action: "copy" | "export") =>
|
||||
props.onConfirm?.({
|
||||
action,
|
||||
format: store.format,
|
||||
debug: store.debug,
|
||||
thinking: store.thinking,
|
||||
toolDetails: store.toolDetails,
|
||||
assistantMetadata: store.assistantMetadata,
|
||||
})
|
||||
|
||||
const activate = () => {
|
||||
if (store.active === "markdown" || store.active === "json") {
|
||||
setStore("format", store.active)
|
||||
return
|
||||
}
|
||||
if (store.active === "debug") setStore("debug", !store.debug)
|
||||
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
||||
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
|
||||
if (store.active === "assistantMetadata") setStore("assistantMetadata", !store.assistantMetadata)
|
||||
if (store.active === "copy" || store.active === "export") confirm(store.active)
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
bindings: [
|
||||
{
|
||||
|
|
@ -40,173 +65,144 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||
desc: "Next export option",
|
||||
group: "Dialog",
|
||||
cmd: () => {
|
||||
const order: Array<"filename" | "thinking" | "toolDetails" | "assistantMetadata" | "openWithoutSaving"> = [
|
||||
"filename",
|
||||
"thinking",
|
||||
"toolDetails",
|
||||
"assistantMetadata",
|
||||
"openWithoutSaving",
|
||||
]
|
||||
const currentIndex = order.indexOf(store.active)
|
||||
const nextIndex = (currentIndex + 1) % order.length
|
||||
setStore("active", order[nextIndex])
|
||||
const order: Active[] =
|
||||
store.format === "markdown"
|
||||
? ["markdown", "json", "thinking", "toolDetails", "assistantMetadata", "copy", "export"]
|
||||
: ["markdown", "json", "debug", "copy", "export"]
|
||||
setStore("active", order[(order.indexOf(store.active) + 1) % order.length])
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
useBindings(() => ({
|
||||
enabled: store.active !== "filename",
|
||||
bindings: [
|
||||
{
|
||||
key: "space",
|
||||
desc: "Toggle export option",
|
||||
key: "return",
|
||||
desc: "Select export option",
|
||||
group: "Dialog",
|
||||
cmd: () => {
|
||||
if (store.active === "thinking") setStore("thinking", !store.thinking)
|
||||
if (store.active === "toolDetails") setStore("toolDetails", !store.toolDetails)
|
||||
if (store.active === "assistantMetadata") setStore("assistantMetadata", !store.assistantMetadata)
|
||||
if (store.active === "openWithoutSaving") setStore("openWithoutSaving", !store.openWithoutSaving)
|
||||
},
|
||||
cmd: activate,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
setTimeout(() => {
|
||||
if (!textarea || textarea.isDestroyed) return
|
||||
textarea.focus()
|
||||
}, 1)
|
||||
textarea.gotoLineEnd()
|
||||
})
|
||||
const selectFormat = (format: ExportFormat) => {
|
||||
setStore("format", format)
|
||||
setStore("active", format)
|
||||
}
|
||||
|
||||
const toggle = (option: "thinking" | "toolDetails" | "assistantMetadata") => {
|
||||
setStore("active", option)
|
||||
setStore(option, !store[option])
|
||||
}
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
Export Options
|
||||
Export session
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box gap={1}>
|
||||
<box>
|
||||
<text fg={theme.text}>Filename:</text>
|
||||
</box>
|
||||
<textarea
|
||||
onSubmit={() => {
|
||||
props.onConfirm?.({
|
||||
filename: textarea.plainText,
|
||||
thinking: store.thinking,
|
||||
toolDetails: store.toolDetails,
|
||||
assistantMetadata: store.assistantMetadata,
|
||||
openWithoutSaving: store.openWithoutSaving,
|
||||
})
|
||||
}}
|
||||
height={3}
|
||||
ref={(val: TextareaRenderable) => {
|
||||
textarea = val
|
||||
val.traits = { status: "FILENAME" }
|
||||
}}
|
||||
initialValue={props.defaultFilename}
|
||||
placeholder="Enter filename"
|
||||
placeholderColor={theme.textMuted}
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.text}
|
||||
/>
|
||||
</box>
|
||||
<box flexDirection="column">
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "thinking" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "thinking")}
|
||||
>
|
||||
<text fg={store.active === "thinking" ? theme.primary : theme.textMuted}>
|
||||
{store.thinking ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "thinking" ? theme.primary : theme.text}>Include thinking</text>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "toolDetails" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "toolDetails")}
|
||||
>
|
||||
<text fg={store.active === "toolDetails" ? theme.primary : theme.textMuted}>
|
||||
{store.toolDetails ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "toolDetails" ? theme.primary : theme.text}>Include tool details</text>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "assistantMetadata" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "assistantMetadata")}
|
||||
>
|
||||
<text fg={store.active === "assistantMetadata" ? theme.primary : theme.textMuted}>
|
||||
{store.assistantMetadata ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "assistantMetadata" ? theme.primary : theme.text}>Include assistant metadata</text>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={2}
|
||||
paddingLeft={1}
|
||||
backgroundColor={store.active === "openWithoutSaving" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => setStore("active", "openWithoutSaving")}
|
||||
>
|
||||
<text fg={store.active === "openWithoutSaving" ? theme.primary : theme.textMuted}>
|
||||
{store.openWithoutSaving ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === "openWithoutSaving" ? theme.primary : theme.text}>Open without saving</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<text fg={theme.text}>Export as:</text>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<For each={["markdown", "json"] as const}>
|
||||
{(format) => (
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
backgroundColor={store.format === format ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => selectFormat(format)}
|
||||
>
|
||||
<text fg={store.format === format ? theme.text : theme.textMuted}>
|
||||
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</box>
|
||||
<Show when={store.active !== "filename"}>
|
||||
<text fg={theme.textMuted} paddingBottom={1}>
|
||||
Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
|
||||
to confirm
|
||||
</text>
|
||||
<Show when={store.format === "markdown"}>
|
||||
<box flexDirection="column">
|
||||
<For
|
||||
each={
|
||||
[
|
||||
["thinking", "Include thinking"],
|
||||
["toolDetails", "Include tool details"],
|
||||
["assistantMetadata", "Include assistant metadata"],
|
||||
] as const
|
||||
}
|
||||
>
|
||||
{(item) => (
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
backgroundColor={store.active === item[0] ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => toggle(item[0])}
|
||||
>
|
||||
<text fg={store.active === item[0] ? theme.primary : theme.textMuted}>
|
||||
{store[item[0]] ? "[x]" : "[ ]"}
|
||||
</text>
|
||||
<text fg={store.active === item[0] ? theme.primary : theme.text}>{item[1]}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={store.active === "filename"}>
|
||||
<text fg={theme.textMuted} paddingBottom={1}>
|
||||
Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
|
||||
for options
|
||||
</text>
|
||||
<Show when={store.format === "json"}>
|
||||
<box
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
backgroundColor={store.active === "debug" ? theme.backgroundElement : undefined}
|
||||
onMouseUp={() => {
|
||||
setStore("active", "debug")
|
||||
setStore("debug", !store.debug)
|
||||
}}
|
||||
>
|
||||
<text fg={store.active === "debug" ? theme.primary : theme.textMuted}>{store.debug ? "[x]" : "[ ]"}</text>
|
||||
<text fg={store.active === "debug" ? theme.primary : theme.text}>Events (debug)</text>
|
||||
</box>
|
||||
</Show>
|
||||
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
||||
<box
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
onMouseUp={() => confirm("copy")}
|
||||
>
|
||||
<text fg={theme.text}>Copy</text>
|
||||
</box>
|
||||
<box
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
backgroundColor={theme.primary}
|
||||
onMouseUp={() => confirm("export")}
|
||||
>
|
||||
<text fg={theme.selectedListItemText}>Export</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogExportOptions.show = (
|
||||
dialog: DialogContext,
|
||||
defaultFilename: string,
|
||||
defaultThinking: boolean,
|
||||
defaultToolDetails: boolean,
|
||||
defaultAssistantMetadata: boolean,
|
||||
defaultOpenWithoutSaving: boolean,
|
||||
) => {
|
||||
return new Promise<{
|
||||
filename: string
|
||||
action: "copy" | "export"
|
||||
format: ExportFormat
|
||||
debug: boolean
|
||||
thinking: boolean
|
||||
toolDetails: boolean
|
||||
assistantMetadata: boolean
|
||||
openWithoutSaving: boolean
|
||||
} | null>((resolve) => {
|
||||
dialog.replace(
|
||||
() => (
|
||||
<DialogExportOptions
|
||||
defaultFilename={defaultFilename}
|
||||
defaultThinking={defaultThinking}
|
||||
defaultToolDetails={defaultToolDetails}
|
||||
defaultAssistantMetadata={defaultAssistantMetadata}
|
||||
defaultOpenWithoutSaving={defaultOpenWithoutSaving}
|
||||
onConfirm={(options) => resolve(options)}
|
||||
onCancel={() => resolve(null)}
|
||||
/>
|
||||
|
|
|
|||
56
packages/tui/src/ui/dialog-export-result.tsx
Normal file
56
packages/tui/src/ui/dialog-export-result.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { useBindings } from "../keymap"
|
||||
import { useDialog, type DialogContext } from "./dialog"
|
||||
|
||||
export function DialogExportResult(props: { path: string; onClose?: () => void }) {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const close = () => {
|
||||
props.onClose?.()
|
||||
dialog.clear()
|
||||
}
|
||||
|
||||
useBindings(() => ({
|
||||
bindings: [
|
||||
{
|
||||
key: "return",
|
||||
desc: "Close export result",
|
||||
group: "Dialog",
|
||||
cmd: close,
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
Session exported
|
||||
</text>
|
||||
<text fg={theme.textMuted} onMouseUp={close}>
|
||||
esc
|
||||
</text>
|
||||
</box>
|
||||
<box>
|
||||
<text fg={theme.text}>{props.path}</text>
|
||||
</box>
|
||||
<box flexDirection="row" justifyContent="flex-end" gap={1} paddingBottom={1}>
|
||||
<box
|
||||
paddingLeft={3}
|
||||
paddingRight={3}
|
||||
backgroundColor={theme.primary}
|
||||
onMouseUp={close}
|
||||
>
|
||||
<text fg={theme.selectedListItemText}>Close</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
DialogExportResult.show = (dialog: DialogContext, path: string) =>
|
||||
new Promise<void>((resolve) => {
|
||||
dialog.replace(() => <DialogExportResult path={path} onClose={resolve} />, resolve)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue