feat(tui): revamp session export (#35971)
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
} from "solid-js"
|
} from "solid-js"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
|
import { EOL, tmpdir } from "node:os"
|
||||||
import { mkdir, writeFile } from "node:fs/promises"
|
import { mkdir, writeFile } from "node:fs/promises"
|
||||||
import { useRoute, useRouteData } from "../../context/route"
|
import { useRoute, useRouteData } from "../../context/route"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -61,6 +62,7 @@ import { normalizePath } from "../../util/path"
|
|||||||
import { PermissionPrompt } from "./permission"
|
import { PermissionPrompt } from "./permission"
|
||||||
import { FormPrompt } from "./form"
|
import { FormPrompt } from "./form"
|
||||||
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||||
|
import { DialogExportResult } from "../../ui/dialog-export-result"
|
||||||
import { sessionEpilogue } from "../../util/presentation"
|
import { sessionEpilogue } from "../../util/presentation"
|
||||||
import { useTuiConfig } from "../../config"
|
import { useTuiConfig } from "../../config"
|
||||||
import { useClipboard } from "../../context/clipboard"
|
import { useClipboard } from "../../context/clipboard"
|
||||||
@@ -711,7 +713,13 @@ export function Session() {
|
|||||||
try {
|
try {
|
||||||
const sessionData = session()
|
const sessionData = session()
|
||||||
if (!sessionData) return
|
if (!sessionData) return
|
||||||
const transcript = formatSessionTranscript(sessionData, messages(), showThinking(), showDetails())
|
const transcript = formatSessionTranscript(
|
||||||
|
sessionData,
|
||||||
|
messages(),
|
||||||
|
showThinking(),
|
||||||
|
showDetails(),
|
||||||
|
showAssistantMetadata(),
|
||||||
|
)
|
||||||
await clipboard.write?.(transcript)
|
await clipboard.write?.(transcript)
|
||||||
toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
|
toast.show({ message: "Session transcript copied to clipboard!", variant: "success" })
|
||||||
} catch {
|
} catch {
|
||||||
@@ -731,53 +739,61 @@ export function Session() {
|
|||||||
try {
|
try {
|
||||||
const sessionData = session()
|
const sessionData = session()
|
||||||
if (!sessionData) return
|
if (!sessionData) return
|
||||||
const defaultFilename = `session-${sessionData.id.slice(0, 8)}.md`
|
|
||||||
|
|
||||||
const options = await DialogExportOptions.show(
|
const options = await DialogExportOptions.show(
|
||||||
dialog,
|
dialog,
|
||||||
defaultFilename,
|
|
||||||
showThinking(),
|
showThinking(),
|
||||||
showDetails(),
|
showDetails(),
|
||||||
showAssistantMetadata(),
|
showAssistantMetadata(),
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (options === null) return
|
if (options === null) return
|
||||||
|
|
||||||
const transcript = formatSessionTranscript(sessionData, messages(), options.thinking, options.toolDetails)
|
const content =
|
||||||
|
options.format === "markdown"
|
||||||
|
? formatSessionTranscript(
|
||||||
|
sessionData,
|
||||||
|
messages(),
|
||||||
|
options.thinking,
|
||||||
|
options.toolDetails,
|
||||||
|
options.assistantMetadata,
|
||||||
|
)
|
||||||
|
: await (async () => {
|
||||||
|
if (options.debug) {
|
||||||
|
const events: unknown[] = []
|
||||||
|
for await (const event of sdk.api.session.log({ sessionID: sessionData.id, follow: false })) {
|
||||||
|
if (event.type !== "log.synced") events.push(event)
|
||||||
|
}
|
||||||
|
return JSON.stringify({ info: sessionData, events }, null, 2) + EOL
|
||||||
|
}
|
||||||
|
|
||||||
if (options.openWithoutSaving) {
|
const messages: unknown[] = []
|
||||||
// Just open in editor without saving
|
let cursor: string | undefined
|
||||||
await openEditor({
|
do {
|
||||||
renderer,
|
const page = await sdk.api.message.list(
|
||||||
value: transcript,
|
cursor
|
||||||
cwd:
|
? { sessionID: sessionData.id, limit: 200, cursor }
|
||||||
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
|
: { sessionID: sessionData.id, limit: 200, order: "asc" },
|
||||||
project.instance.directory() ||
|
)
|
||||||
paths.cwd,
|
messages.push(...page.data)
|
||||||
})
|
cursor = page.data.length ? (page.cursor.next ?? undefined) : undefined
|
||||||
} else {
|
} while (cursor)
|
||||||
const exportDir = paths.cwd
|
return JSON.stringify({ info: sessionData, messages }, null, 2) + EOL
|
||||||
const filename = options.filename.trim()
|
})()
|
||||||
const filepath = path.join(exportDir, filename)
|
|
||||||
|
|
||||||
await writeExport(filepath, transcript)
|
if (options.action === "copy") {
|
||||||
|
await clipboard.write?.(content)
|
||||||
// Open with EDITOR if available
|
dialog.clear()
|
||||||
const result = await openEditor({
|
toast.show({ message: "Copied to clipboard", variant: "success" })
|
||||||
renderer,
|
return
|
||||||
value: transcript,
|
|
||||||
cwd:
|
|
||||||
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
|
|
||||||
project.instance.directory() ||
|
|
||||||
paths.cwd,
|
|
||||||
})
|
|
||||||
if (result !== undefined) {
|
|
||||||
await writeExport(filepath, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.show({ message: `Session exported to ${filename}`, variant: "success" })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filepath = path.join(
|
||||||
|
tmpdir(),
|
||||||
|
`session-${crypto.randomUUID()}.${options.format === "markdown" ? "md" : "json"}`,
|
||||||
|
)
|
||||||
|
await writeExport(filepath, content)
|
||||||
|
await DialogExportResult.show(dialog, filepath)
|
||||||
} catch {
|
} catch {
|
||||||
toast.show({ message: "Failed to export session", variant: "error" })
|
toast.show({ message: "Failed to export session", variant: "error" })
|
||||||
}
|
}
|
||||||
@@ -2746,6 +2762,7 @@ function formatSessionTranscript(
|
|||||||
messages: SessionMessageInfo[],
|
messages: SessionMessageInfo[],
|
||||||
thinking: boolean,
|
thinking: boolean,
|
||||||
toolDetails: boolean,
|
toolDetails: boolean,
|
||||||
|
assistantMetadata: boolean,
|
||||||
) {
|
) {
|
||||||
const body = messages.flatMap((message) => {
|
const body = messages.flatMap((message) => {
|
||||||
if (message.type === "user") return [`## User\n\n${message.text}`]
|
if (message.type === "user") return [`## User\n\n${message.text}`]
|
||||||
@@ -2767,7 +2784,13 @@ function formatSessionTranscript(
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
return [`**Tool: ${item.name}**\n\n**Input:**\n\`\`\`json\n${input}\n\`\`\`\n\n${output}`]
|
return [`**Tool: ${item.name}**\n\n**Input:**\n\`\`\`json\n${input}\n\`\`\`\n\n${output}`]
|
||||||
})
|
})
|
||||||
return [`## Assistant\n\n${content.join("\n\n")}`]
|
const duration = message.time.completed
|
||||||
|
? ` · ${((message.time.completed - message.time.created) / 1000).toFixed(1)}s`
|
||||||
|
: ""
|
||||||
|
const heading = assistantMetadata
|
||||||
|
? `## Assistant (${message.agent} · ${message.model.providerID}/${message.model.id}${duration})`
|
||||||
|
: "## Assistant"
|
||||||
|
return [`${heading}\n\n${content.join("\n\n")}`]
|
||||||
})
|
})
|
||||||
return `# ${session.title}\n\n**Session ID:** ${session.id}\n**Created:** ${new Date(session.time.created).toLocaleString()}\n**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n---\n\n${body.join("\n\n---\n\n")}\n`
|
return `# ${session.title}\n\n**Session ID:** ${session.id}\n**Created:** ${new Date(session.time.created).toLocaleString()}\n**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n---\n\n${body.join("\n\n---\n\n")}\n`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,63 @@
|
|||||||
import { TextareaRenderable, TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useDialog, type DialogContext } from "./dialog"
|
import { useDialog, type DialogContext } from "./dialog"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { onMount, Show } from "solid-js"
|
import { For, Show } from "solid-js"
|
||||||
import { useBindings } from "../keymap"
|
import { useBindings } from "../keymap"
|
||||||
|
|
||||||
|
export type ExportFormat = "markdown" | "json"
|
||||||
|
|
||||||
export type DialogExportOptionsProps = {
|
export type DialogExportOptionsProps = {
|
||||||
defaultFilename: string
|
|
||||||
defaultThinking: boolean
|
defaultThinking: boolean
|
||||||
defaultToolDetails: boolean
|
defaultToolDetails: boolean
|
||||||
defaultAssistantMetadata: boolean
|
defaultAssistantMetadata: boolean
|
||||||
defaultOpenWithoutSaving: boolean
|
|
||||||
onConfirm?: (options: {
|
onConfirm?: (options: {
|
||||||
filename: string
|
action: "copy" | "export"
|
||||||
|
format: ExportFormat
|
||||||
|
debug: boolean
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
toolDetails: boolean
|
toolDetails: boolean
|
||||||
assistantMetadata: boolean
|
assistantMetadata: boolean
|
||||||
openWithoutSaving: boolean
|
|
||||||
}) => void
|
}) => void
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Active = ExportFormat | "debug" | "thinking" | "toolDetails" | "assistantMetadata" | "copy" | "export"
|
||||||
|
|
||||||
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
export function DialogExportOptions(props: DialogExportOptionsProps) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
let textarea: TextareaRenderable
|
|
||||||
const [store, setStore] = createStore({
|
const [store, setStore] = createStore({
|
||||||
|
format: "markdown" as ExportFormat,
|
||||||
|
debug: false,
|
||||||
thinking: props.defaultThinking,
|
thinking: props.defaultThinking,
|
||||||
toolDetails: props.defaultToolDetails,
|
toolDetails: props.defaultToolDetails,
|
||||||
assistantMetadata: props.defaultAssistantMetadata,
|
assistantMetadata: props.defaultAssistantMetadata,
|
||||||
openWithoutSaving: props.defaultOpenWithoutSaving,
|
active: "markdown" as Active,
|
||||||
active: "filename" as "filename" | "thinking" | "toolDetails" | "assistantMetadata" | "openWithoutSaving",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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(() => ({
|
useBindings(() => ({
|
||||||
bindings: [
|
bindings: [
|
||||||
{
|
{
|
||||||
@@ -40,173 +65,144 @@ export function DialogExportOptions(props: DialogExportOptionsProps) {
|
|||||||
desc: "Next export option",
|
desc: "Next export option",
|
||||||
group: "Dialog",
|
group: "Dialog",
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
const order: Array<"filename" | "thinking" | "toolDetails" | "assistantMetadata" | "openWithoutSaving"> = [
|
const order: Active[] =
|
||||||
"filename",
|
store.format === "markdown"
|
||||||
"thinking",
|
? ["markdown", "json", "thinking", "toolDetails", "assistantMetadata", "copy", "export"]
|
||||||
"toolDetails",
|
: ["markdown", "json", "debug", "copy", "export"]
|
||||||
"assistantMetadata",
|
setStore("active", order[(order.indexOf(store.active) + 1) % order.length])
|
||||||
"openWithoutSaving",
|
|
||||||
]
|
|
||||||
const currentIndex = order.indexOf(store.active)
|
|
||||||
const nextIndex = (currentIndex + 1) % order.length
|
|
||||||
setStore("active", order[nextIndex])
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
|
||||||
}))
|
|
||||||
|
|
||||||
useBindings(() => ({
|
|
||||||
enabled: store.active !== "filename",
|
|
||||||
bindings: [
|
|
||||||
{
|
{
|
||||||
key: "space",
|
key: "return",
|
||||||
desc: "Toggle export option",
|
desc: "Select export option",
|
||||||
group: "Dialog",
|
group: "Dialog",
|
||||||
cmd: () => {
|
cmd: activate,
|
||||||
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)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}))
|
}))
|
||||||
|
|
||||||
onMount(() => {
|
const selectFormat = (format: ExportFormat) => {
|
||||||
dialog.setSize("medium")
|
setStore("format", format)
|
||||||
setTimeout(() => {
|
setStore("active", format)
|
||||||
if (!textarea || textarea.isDestroyed) return
|
}
|
||||||
textarea.focus()
|
|
||||||
}, 1)
|
const toggle = (option: "thinking" | "toolDetails" | "assistantMetadata") => {
|
||||||
textarea.gotoLineEnd()
|
setStore("active", option)
|
||||||
})
|
setStore(option, !store[option])
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box paddingLeft={2} paddingRight={2} gap={1}>
|
<box paddingLeft={2} paddingRight={2} gap={1}>
|
||||||
<box flexDirection="row" justifyContent="space-between">
|
<box flexDirection="row" justifyContent="space-between">
|
||||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||||
Export Options
|
Export session
|
||||||
</text>
|
</text>
|
||||||
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
|
||||||
esc
|
esc
|
||||||
</text>
|
</text>
|
||||||
</box>
|
</box>
|
||||||
<box gap={1}>
|
<box flexDirection="row" gap={1}>
|
||||||
<box>
|
<text fg={theme.text}>Export as:</text>
|
||||||
<text fg={theme.text}>Filename:</text>
|
<box flexDirection="row" gap={1}>
|
||||||
</box>
|
<For each={["markdown", "json"] as const}>
|
||||||
<textarea
|
{(format) => (
|
||||||
onSubmit={() => {
|
<box
|
||||||
props.onConfirm?.({
|
paddingLeft={1}
|
||||||
filename: textarea.plainText,
|
paddingRight={1}
|
||||||
thinking: store.thinking,
|
backgroundColor={store.format === format ? theme.backgroundElement : undefined}
|
||||||
toolDetails: store.toolDetails,
|
onMouseUp={() => selectFormat(format)}
|
||||||
assistantMetadata: store.assistantMetadata,
|
>
|
||||||
openWithoutSaving: store.openWithoutSaving,
|
<text fg={store.format === format ? theme.text : theme.textMuted}>
|
||||||
})
|
{store.format === format ? "◉" : "○"} {format === "markdown" ? "Markdown" : "JSON"}
|
||||||
}}
|
</text>
|
||||||
height={3}
|
</box>
|
||||||
ref={(val: TextareaRenderable) => {
|
)}
|
||||||
textarea = val
|
</For>
|
||||||
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>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
<Show when={store.active !== "filename"}>
|
<Show when={store.format === "markdown"}>
|
||||||
<text fg={theme.textMuted} paddingBottom={1}>
|
<box flexDirection="column">
|
||||||
Press <span style={{ fg: theme.text }}>space</span> to toggle, <span style={{ fg: theme.text }}>return</span>{" "}
|
<For
|
||||||
to confirm
|
each={
|
||||||
</text>
|
[
|
||||||
|
["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>
|
||||||
<Show when={store.active === "filename"}>
|
<Show when={store.format === "json"}>
|
||||||
<text fg={theme.textMuted} paddingBottom={1}>
|
<box
|
||||||
Press <span style={{ fg: theme.text }}>return</span> to confirm, <span style={{ fg: theme.text }}>tab</span>{" "}
|
flexDirection="row"
|
||||||
for options
|
gap={1}
|
||||||
</text>
|
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>
|
</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>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogExportOptions.show = (
|
DialogExportOptions.show = (
|
||||||
dialog: DialogContext,
|
dialog: DialogContext,
|
||||||
defaultFilename: string,
|
|
||||||
defaultThinking: boolean,
|
defaultThinking: boolean,
|
||||||
defaultToolDetails: boolean,
|
defaultToolDetails: boolean,
|
||||||
defaultAssistantMetadata: boolean,
|
defaultAssistantMetadata: boolean,
|
||||||
defaultOpenWithoutSaving: boolean,
|
|
||||||
) => {
|
) => {
|
||||||
return new Promise<{
|
return new Promise<{
|
||||||
filename: string
|
action: "copy" | "export"
|
||||||
|
format: ExportFormat
|
||||||
|
debug: boolean
|
||||||
thinking: boolean
|
thinking: boolean
|
||||||
toolDetails: boolean
|
toolDetails: boolean
|
||||||
assistantMetadata: boolean
|
assistantMetadata: boolean
|
||||||
openWithoutSaving: boolean
|
|
||||||
} | null>((resolve) => {
|
} | null>((resolve) => {
|
||||||
dialog.replace(
|
dialog.replace(
|
||||||
() => (
|
() => (
|
||||||
<DialogExportOptions
|
<DialogExportOptions
|
||||||
defaultFilename={defaultFilename}
|
|
||||||
defaultThinking={defaultThinking}
|
defaultThinking={defaultThinking}
|
||||||
defaultToolDetails={defaultToolDetails}
|
defaultToolDetails={defaultToolDetails}
|
||||||
defaultAssistantMetadata={defaultAssistantMetadata}
|
defaultAssistantMetadata={defaultAssistantMetadata}
|
||||||
defaultOpenWithoutSaving={defaultOpenWithoutSaving}
|
|
||||||
onConfirm={(options) => resolve(options)}
|
onConfirm={(options) => resolve(options)}
|
||||||
onCancel={() => resolve(null)}
|
onCancel={() => resolve(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user