mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-17 21:21:18 -04:00
refactor(tui): reuse session question panel
This commit is contained in:
@@ -1,18 +1,17 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { createSignal, For, Show } from "solid-js"
|
||||
import { createSignal } from "solid-js"
|
||||
import { SessionLocationMissing } from "../../../routes/session/location-missing"
|
||||
import type { Story } from "./index"
|
||||
import { StoryFooter } from "./footer"
|
||||
|
||||
const missing = "~/code/open-source/opencode-workerd-profile"
|
||||
const destinations = ["~/code/open-source/opencode", "~/code/open-source/opencode-instruction-rename"]
|
||||
const directory = "/Users/kit/code/open-source/opencode-workerd-profile"
|
||||
|
||||
function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
const dimensions = useTerminalDimensions()
|
||||
const theme = props.context.theme.contextual.elevated
|
||||
const [state, setState] = createSignal<"missing" | "move" | "recovered">("missing")
|
||||
const [selected, setSelected] = createSignal(0)
|
||||
const [message, setMessage] = createSignal("Select a recovery action")
|
||||
|
||||
props.context.keymap.layer(() => ({
|
||||
commands: [
|
||||
@@ -22,43 +21,15 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
group: "Storybook",
|
||||
run: () => props.context.ui.router.navigate({ type: "plugin", name: "storybook" }),
|
||||
},
|
||||
{
|
||||
bind: "return",
|
||||
title: "Continue",
|
||||
group: "Storybook",
|
||||
run: () => {
|
||||
if (state() === "missing") return setState("move")
|
||||
if (state() === "move") return setState("recovered")
|
||||
},
|
||||
},
|
||||
{
|
||||
bind: "up,k",
|
||||
title: "Previous destination",
|
||||
group: "Storybook",
|
||||
run: () => state() === "move" && setSelected((current) => (current + destinations.length - 1) % destinations.length),
|
||||
},
|
||||
{
|
||||
bind: "down,j",
|
||||
title: "Next destination",
|
||||
group: "Storybook",
|
||||
run: () => state() === "move" && setSelected((current) => (current + 1) % destinations.length),
|
||||
},
|
||||
{
|
||||
bind: "r",
|
||||
title: "Reset story",
|
||||
group: "Storybook",
|
||||
run: () => {
|
||||
setSelected(0)
|
||||
setState("missing")
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
return (
|
||||
<box width={dimensions().width} height={dimensions().height} backgroundColor={theme.background.default}>
|
||||
<box paddingLeft={2} paddingRight={2} paddingTop={1} flexGrow={1}>
|
||||
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>Workerd Modal workspace driver</text>
|
||||
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>
|
||||
Workerd Modal workspace driver
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>build · GPT-5.6 Sol (high)</text>
|
||||
<box height={1} />
|
||||
<text fg={theme.text.default}>You</text>
|
||||
@@ -67,47 +38,15 @@ function SessionLocationMissingStory(props: { context: Plugin.Context }) {
|
||||
<text fg={theme.text.default}>Build · GPT-5.6 Sol (high)</text>
|
||||
<text fg={theme.text.subdued}>The deployment is verified and the worktree is clean.</text>
|
||||
<box flexGrow={1} />
|
||||
<Show when={state() === "missing"}>
|
||||
<box border={["left"]} borderColor={theme.text.feedback.warning.default} paddingLeft={2} gap={1}>
|
||||
<text fg={theme.text.feedback.warning.default} attributes={TextAttributes.BOLD}>
|
||||
Session directory no longer exists
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>{missing}</text>
|
||||
<text fg={theme.text.default}>Move this session to continue.</text>
|
||||
<text fg={theme.text.action.primary.default}>[ Move session ] <span style={{ fg: theme.text.subdued }}>[ Copy path ]</span></text>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={state() === "move"}>
|
||||
<box border={["left"]} borderColor={theme.border.default} paddingLeft={2} gap={1}>
|
||||
<text fg={theme.text.default} attributes={TextAttributes.BOLD}>Move session</text>
|
||||
<text fg={theme.text.subdued}>Choose a project directory</text>
|
||||
<For each={destinations}>
|
||||
{(directory, index) => (
|
||||
<text fg={index() === selected() ? theme.text.action.primary.focused : theme.text.subdued}>
|
||||
{index() === selected() ? "› " : " "}{directory}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={state() === "recovered"}>
|
||||
<box border={["left"]} borderColor={theme.text.feedback.success.default} paddingLeft={2} gap={1}>
|
||||
<text fg={theme.text.feedback.success.default} attributes={TextAttributes.BOLD}>Session moved</text>
|
||||
<text fg={theme.text.subdued}>{destinations[selected()]}</text>
|
||||
<box border borderStyle="rounded" borderColor={theme.border.default} paddingLeft={1} paddingRight={1}>
|
||||
<text fg={theme.text.subdued}>Ask anything...</text>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
<SessionLocationMissing directory={directory} onMove={() => setMessage("Move session requested")} />
|
||||
</box>
|
||||
<StoryFooter
|
||||
context={props.context}
|
||||
title="storybook / missing session directory"
|
||||
details={[state()]}
|
||||
status={message()}
|
||||
controls={[
|
||||
{ shortcut: "enter", label: state() === "move" ? "move" : "continue" },
|
||||
{ shortcut: "↑/↓", label: "destination" },
|
||||
{ shortcut: "r", label: "reset" },
|
||||
{ shortcut: "←/→", label: "select" },
|
||||
{ shortcut: "enter", label: "confirm" },
|
||||
{ shortcut: "esc", label: "back" },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -109,7 +109,7 @@ import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
|
||||
import { generateThinkingSyntax } from "./thinking-syntax"
|
||||
import { createDelayedPresence } from "../../util/delayed-presence"
|
||||
import { usePromptMove } from "../../component/prompt/move"
|
||||
import { abbreviateHome } from "../../util/path-format"
|
||||
import { SessionLocationMissing } from "./location-missing"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -1169,52 +1169,6 @@ export function Session() {
|
||||
)
|
||||
}
|
||||
|
||||
function SessionLocationMissing(props: { directory: string; onMove: () => void }) {
|
||||
const theme = useTheme()
|
||||
const paths = useTuiPaths()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72))
|
||||
|
||||
return (
|
||||
<box
|
||||
border={["left"]}
|
||||
borderColor={theme.text.feedback.warning.default}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
paddingTop={1}
|
||||
paddingBottom={1}
|
||||
gap={1}
|
||||
>
|
||||
<text fg={theme.text.feedback.warning.default} attributes={TextAttributes.BOLD}>
|
||||
Session directory no longer exists
|
||||
</text>
|
||||
<text fg={theme.text.subdued}>{directory()}</text>
|
||||
<text fg={theme.text.default}>Move this session to continue.</text>
|
||||
<box flexDirection="row" gap={2}>
|
||||
<box
|
||||
backgroundColor={theme.background.action.primary.default}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
onMouseUp={props.onMove}
|
||||
>
|
||||
<text fg={theme.text.action.primary.default}>Move session</text>
|
||||
</box>
|
||||
<box
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
onMouseUp={() => {
|
||||
void clipboard.write(props.directory)
|
||||
toast.show({ message: "Copied session directory", variant: "success" })
|
||||
}}
|
||||
>
|
||||
<text fg={theme.text.subdued}>Copy path</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
type SessionRowViewProps = {
|
||||
row: SessionRow
|
||||
message: (messageID: string) => SessionMessageInfo | undefined
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { createMemo } from "solid-js"
|
||||
import { useClipboard } from "../../context/clipboard"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { abbreviateHome } from "../../util/path-format"
|
||||
import { SessionQuestion } from "./permission"
|
||||
|
||||
export function SessionLocationMissing(props: { directory: string; onMove: () => void }) {
|
||||
const paths = useTuiPaths()
|
||||
const clipboard = useClipboard()
|
||||
const toast = useToast()
|
||||
const theme = useTheme("elevated")
|
||||
const directory = createMemo(() => Locale.truncateMiddle(abbreviateHome(props.directory, paths.home), 72))
|
||||
|
||||
return (
|
||||
<SessionQuestion
|
||||
id="session.location-missing"
|
||||
group="Session recovery"
|
||||
choicesLabel="Recovery actions"
|
||||
instance={props.directory}
|
||||
title="Session directory no longer exists"
|
||||
body={
|
||||
<box paddingLeft={1} gap={1}>
|
||||
<text fg={theme.text.subdued}>{directory()}</text>
|
||||
<text fg={theme.text.default}>Move this session to continue.</text>
|
||||
</box>
|
||||
}
|
||||
options={{ move: "Move session", copy: "Copy path" }}
|
||||
onSelect={(option) => {
|
||||
if (option === "move") return props.onMove()
|
||||
void clipboard.write(props.directory)
|
||||
toast.show({ message: "Copied session directory", variant: "success" })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.stage === "always"}>
|
||||
<Prompt
|
||||
<SessionQuestion
|
||||
title="Always allow"
|
||||
semanticLabel={`Always allow ${props.request.action}`}
|
||||
instance={props.request.id}
|
||||
@@ -235,7 +235,7 @@ export function PermissionPrompt(props: { request: PermissionRequest; directory?
|
||||
)
|
||||
|
||||
const body = (
|
||||
<Prompt
|
||||
<SessionQuestion
|
||||
title="Permission required"
|
||||
semanticLabel={permissionSemanticLabel(props.request.action, current.title)}
|
||||
instance={props.request.id}
|
||||
@@ -411,10 +411,13 @@ function RejectPrompt(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function Prompt<const T extends Record<string, string>>(props: {
|
||||
export function SessionQuestion<const T extends Record<string, string>>(props: {
|
||||
title: string
|
||||
semanticLabel?: string
|
||||
instance: string
|
||||
id?: string
|
||||
group?: string
|
||||
choicesLabel?: string
|
||||
header?: JSX.Element
|
||||
body: JSX.Element
|
||||
options: T
|
||||
@@ -431,6 +434,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
})
|
||||
const narrow = createMemo(() => dimensions().width < 80)
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const id = () => props.id ?? "session.permission"
|
||||
const group = () => props.group ?? "Permission"
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "base",
|
||||
@@ -438,7 +443,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
{
|
||||
id: "app.exit",
|
||||
title: "Reject permission",
|
||||
group: "Permission",
|
||||
group: group(),
|
||||
bind: false,
|
||||
run() {
|
||||
if (!props.escapeKey) return
|
||||
@@ -448,7 +453,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
{
|
||||
id: "permission.prompt.fullscreen",
|
||||
title: "Toggle permission fullscreen",
|
||||
group: "Permission",
|
||||
group: group(),
|
||||
bind: false,
|
||||
run() {
|
||||
if (!props.fullscreen) return
|
||||
@@ -457,8 +462,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
},
|
||||
{
|
||||
bind: "left",
|
||||
title: "Previous permission option",
|
||||
group: "Permission",
|
||||
title: "Previous option",
|
||||
group: group(),
|
||||
run: () => {
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx - 1 + keys.length) % keys.length]
|
||||
@@ -467,8 +472,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
},
|
||||
{
|
||||
bind: "h",
|
||||
title: "Previous permission option",
|
||||
group: "Permission",
|
||||
title: "Previous option",
|
||||
group: group(),
|
||||
run: () => {
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx - 1 + keys.length) % keys.length]
|
||||
@@ -477,8 +482,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
},
|
||||
{
|
||||
bind: "right",
|
||||
title: "Next permission option",
|
||||
group: "Permission",
|
||||
title: "Next option",
|
||||
group: group(),
|
||||
run: () => {
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx + 1) % keys.length]
|
||||
@@ -487,8 +492,8 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
},
|
||||
{
|
||||
bind: "l",
|
||||
title: "Next permission option",
|
||||
group: "Permission",
|
||||
title: "Next option",
|
||||
group: group(),
|
||||
run: () => {
|
||||
const idx = keys.indexOf(store.selected)
|
||||
const next = keys[(idx + 1) % keys.length]
|
||||
@@ -497,7 +502,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
},
|
||||
{
|
||||
bind: "return",
|
||||
title: "Select permission option",
|
||||
title: "Select option",
|
||||
group: "Permission",
|
||||
run: () => props.onSelect(store.selected),
|
||||
},
|
||||
@@ -506,7 +511,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
{
|
||||
bind: "escape",
|
||||
title: "Reject permission",
|
||||
group: "Permission",
|
||||
group: group(),
|
||||
run: () => props.onSelect(props.escapeKey!),
|
||||
},
|
||||
]
|
||||
@@ -520,7 +525,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
|
||||
const content = () => (
|
||||
<box
|
||||
id="session.permission"
|
||||
id={id()}
|
||||
ref={SimulationSemantics.bind(() => ({
|
||||
instance: props.instance,
|
||||
role: "dialog",
|
||||
@@ -571,11 +576,11 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
alignItems={narrow() ? "flex-start" : "center"}
|
||||
>
|
||||
<box
|
||||
id="session.permission.actions"
|
||||
id={`${id()}.actions`}
|
||||
ref={SimulationSemantics.bind(() => ({
|
||||
instance: props.instance,
|
||||
role: "listbox",
|
||||
label: "Permission choices",
|
||||
label: props.choicesLabel ?? "Permission choices",
|
||||
}))}
|
||||
flexDirection="row"
|
||||
gap={1}
|
||||
@@ -584,7 +589,7 @@ function Prompt<const T extends Record<string, string>>(props: {
|
||||
<For each={keys}>
|
||||
{(option) => (
|
||||
<box
|
||||
id={`session.permission.action.${String(option)}`}
|
||||
id={`${id()}.action.${String(option)}`}
|
||||
ref={SimulationSemantics.bind(() => ({
|
||||
instance: props.instance,
|
||||
role: "option",
|
||||
|
||||
Reference in New Issue
Block a user