refactor(tui): centralize active location
This commit is contained in:
@@ -172,6 +172,7 @@ export interface UI {
|
||||
|
||||
export interface Context {
|
||||
readonly options: Readonly<Record<string, any>>
|
||||
readonly location: LocationRef | undefined
|
||||
readonly client: OpenCodeClient
|
||||
readonly data: Data
|
||||
readonly keymap: Keymap
|
||||
|
||||
@@ -333,6 +333,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<PermissionProvider>
|
||||
<ProjectProvider>
|
||||
<DataProvider>
|
||||
<LocationProvider>
|
||||
<ThemeProvider mode={mode}>
|
||||
<LocalProvider>
|
||||
<PromptStashProvider>
|
||||
@@ -341,7 +342,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
<PromptHistoryProvider>
|
||||
<PromptRefProvider>
|
||||
<EditorContextProvider>
|
||||
<LocationProvider>
|
||||
<PluginProvider packages={input.packages}>
|
||||
<App
|
||||
pair={
|
||||
@@ -354,7 +354,6 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
}
|
||||
/>
|
||||
</PluginProvider>
|
||||
</LocationProvider>
|
||||
</EditorContextProvider>
|
||||
</PromptRefProvider>
|
||||
</PromptHistoryProvider>
|
||||
@@ -363,6 +362,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
||||
</PromptStashProvider>
|
||||
</LocalProvider>
|
||||
</ThemeProvider>
|
||||
</LocationProvider>
|
||||
</DataProvider>
|
||||
</ProjectProvider>
|
||||
</PermissionProvider>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useClient } from "../../context/client"
|
||||
import { useToast } from "../../ui/toast"
|
||||
import { DialogMoveSession, type MoveSessionSelection } from "../dialog-move-session"
|
||||
import { DialogWorkspaceFileChanges } from "../dialog-workspace-file-changes"
|
||||
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
||||
import { useProject } from "../../context/project"
|
||||
import { useData } from "../../context/data"
|
||||
|
||||
@@ -19,13 +18,13 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
const dialog = useDialog()
|
||||
const client = useClient()
|
||||
const toast = useToast()
|
||||
const homeDestination = useHomeSessionDestination()
|
||||
const project = useProject()
|
||||
const data = useData()
|
||||
const paths = useTuiPaths()
|
||||
const [creating, setCreating] = createSignal(false)
|
||||
const [creatingDots, setCreatingDots] = createSignal(3)
|
||||
const [progress, setProgress] = createSignal<string>()
|
||||
const [destination, setDestination] = createSignal<MoveSessionSelection>()
|
||||
|
||||
async function create(name: string) {
|
||||
const projectID = await resolveProjectID()
|
||||
@@ -49,7 +48,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
setProgress("Creating session")
|
||||
return directory
|
||||
} catch (err) {
|
||||
homeDestination?.clear()
|
||||
setDestination(undefined)
|
||||
setProgress(undefined)
|
||||
setCreating(false)
|
||||
toast.show({ title: "Creating workspace failed", message: errorMessage(err), variant: "error" })
|
||||
@@ -69,7 +68,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
<DialogMoveSession
|
||||
projectID={projectID}
|
||||
current={
|
||||
homeDestination?.destination() ??
|
||||
destination() ??
|
||||
(session
|
||||
? {
|
||||
type: "directory",
|
||||
@@ -82,11 +81,11 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
subdirectory: project.instance.directory() !== project.instance.path().worktree,
|
||||
})
|
||||
}
|
||||
onCurrentChange={(selection) => homeDestination?.setDestination(selection)}
|
||||
onCurrentChange={setDestination}
|
||||
onSelect={(selection) => {
|
||||
const sessionID = input.sessionID()
|
||||
if (!sessionID) {
|
||||
homeDestination?.setDestination(selection)
|
||||
setDestination(selection)
|
||||
dialog.clear()
|
||||
return
|
||||
}
|
||||
@@ -144,11 +143,11 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
return data.session.get(sessionID)
|
||||
}
|
||||
|
||||
const pending = createMemo(() => Boolean(homeDestination?.destination()))
|
||||
const pendingNew = createMemo(() => homeDestination?.destination()?.type === "new")
|
||||
const pending = createMemo(() => Boolean(destination()))
|
||||
const pendingNew = createMemo(() => destination()?.type === "new")
|
||||
|
||||
async function getDirectory() {
|
||||
const value = homeDestination?.destination()
|
||||
const value = destination()
|
||||
if (!value) return
|
||||
if (value.type === "directory") {
|
||||
return value.directory
|
||||
@@ -161,7 +160,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
||||
}
|
||||
|
||||
function finishSubmit() {
|
||||
homeDestination?.clear()
|
||||
setDestination(undefined)
|
||||
setProgress(undefined)
|
||||
setCreating(false)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import type { LocationRef } from "@opencode-ai/client"
|
||||
import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
|
||||
import { createContext, createSignal, useContext, type Accessor, type ParentProps, type Setter } from "solid-js"
|
||||
|
||||
const context = createContext<Accessor<LocationRef | undefined>>()
|
||||
const context = createContext<{
|
||||
current: Accessor<LocationRef | undefined>
|
||||
set: Setter<LocationRef | undefined>
|
||||
}>()
|
||||
|
||||
export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) {
|
||||
return <context.Provider value={() => props.location}>{props.children}</context.Provider>
|
||||
export function LocationProvider(props: ParentProps) {
|
||||
const [current, set] = createSignal<LocationRef>()
|
||||
return <context.Provider value={{ current, set }}>{props.children}</context.Provider>
|
||||
}
|
||||
|
||||
export function useLocation() {
|
||||
const value = useContext(context)
|
||||
if (!value) throw new Error("Location context must be used within a LocationProvider")
|
||||
return value
|
||||
return value.current
|
||||
}
|
||||
|
||||
export function useSetLocation() {
|
||||
const value = useContext(context)
|
||||
if (!value) throw new Error("Location context must be used within a LocationProvider")
|
||||
return value.set
|
||||
}
|
||||
|
||||
@@ -4,19 +4,15 @@ import { createMemo, Match, Show, Switch } from "solid-js"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import { useHomeSessionDestination } from "../../routes/home/session-destination"
|
||||
import { abbreviateHome } from "../../runtime"
|
||||
import { FilePath } from "../../ui/file-path"
|
||||
|
||||
function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||
const { theme } = useTheme()
|
||||
const destination = useHomeSessionDestination()
|
||||
const paths = useTuiPaths()
|
||||
const directory = createMemo(() => {
|
||||
const selected = destination?.destination()
|
||||
if (!selected || selected.type === "new") return
|
||||
return abbreviateHome(selected.directory || props.context.data.location.default().directory, paths.home)
|
||||
})
|
||||
const directory = createMemo(() =>
|
||||
props.context.location ? abbreviateHome(props.context.location.directory, paths.home) : undefined,
|
||||
)
|
||||
|
||||
return (
|
||||
<Show when={directory()}>
|
||||
@@ -27,7 +23,7 @@ function Directory(props: { context: Plugin.Context; maxWidth: number }) {
|
||||
|
||||
function Mcp(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const list = createMemo(() => props.context.data.location.mcp.server.list() ?? [])
|
||||
const list = createMemo(() => props.context.data.location.mcp.server.list(props.context.location) ?? [])
|
||||
const failed = createMemo(() => list().some((item) => item.status.status === "failed"))
|
||||
const count = createMemo(() => list().filter((item) => item.status.status === "connected").length)
|
||||
|
||||
@@ -55,7 +51,7 @@ function View(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const mcpWidth = createMemo(() => {
|
||||
const list = props.context.data.location.mcp.server.list() ?? []
|
||||
const list = props.context.data.location.mcp.server.list(props.context.location) ?? []
|
||||
if (list.length === 0) return 0
|
||||
const count = list.filter((item) => item.status.status === "connected").length
|
||||
return Bun.stringWidth(`⊙ ${count} MCP /status`) + 2
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useData } from "../context/data"
|
||||
import { Keymap } from "../context/keymap"
|
||||
import { useRoute } from "../context/route"
|
||||
import { useTuiLifecycle } from "../context/runtime"
|
||||
import { useLocation } from "../context/location"
|
||||
import { builtins } from "./builtins"
|
||||
|
||||
export interface PackageResolver {
|
||||
@@ -63,6 +64,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
|
||||
const keymap = Keymap.use()
|
||||
const shortcuts = Keymap.useShortcuts()
|
||||
const lifecycle = useTuiLifecycle()
|
||||
const location = useLocation()
|
||||
const directory = config.path ? path.dirname(config.path) : process.cwd()
|
||||
const [store, setStore] = createStore({
|
||||
ready: false,
|
||||
@@ -82,6 +84,9 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
|
||||
const owned: Dispose[] = []
|
||||
const context: Context = {
|
||||
options: item.options ?? {},
|
||||
get location() {
|
||||
return location()
|
||||
},
|
||||
client: client.api,
|
||||
data,
|
||||
keymap: {
|
||||
|
||||
@@ -8,9 +8,8 @@ import { usePromptRef } from "../context/prompt"
|
||||
import { useLocal } from "../context/local"
|
||||
import { usePluginRuntime } from "../plugin/runtime"
|
||||
import { useEditorContext } from "../context/editor"
|
||||
import { HomeSessionDestinationProvider } from "./home/session-destination"
|
||||
import { useData } from "../context/data"
|
||||
import { LocationProvider } from "../context/location"
|
||||
import { useSetLocation } from "../context/location"
|
||||
import { FormPrompt } from "./session/form"
|
||||
import { PluginSlot } from "../plugin/context"
|
||||
|
||||
@@ -29,10 +28,13 @@ export function Home() {
|
||||
const local = useLocal()
|
||||
const editor = useEditorContext()
|
||||
const data = useData()
|
||||
const setLocation = useSetLocation()
|
||||
// Global MCP elicitations can arrive without a session route, so keep them reachable from Home.
|
||||
const forms = createMemo(() => data.session.form.list("global", data.location.default()) ?? [])
|
||||
let sent = false
|
||||
|
||||
createEffect(() => setLocation(data.location.default()))
|
||||
|
||||
onMount(() => {
|
||||
editor.clearSelection()
|
||||
})
|
||||
@@ -64,8 +66,7 @@ export function Home() {
|
||||
})
|
||||
|
||||
return (
|
||||
<LocationProvider location={data.location.default()}>
|
||||
<HomeSessionDestinationProvider>
|
||||
<>
|
||||
<box flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
|
||||
<box flexGrow={1} minHeight={0} />
|
||||
<box height={4} minHeight={0} flexShrink={1} />
|
||||
@@ -104,7 +105,6 @@ export function Home() {
|
||||
) : null
|
||||
}}
|
||||
</Show>
|
||||
</HomeSessionDestinationProvider>
|
||||
</LocationProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
createContext,
|
||||
createMemo,
|
||||
createSignal,
|
||||
useContext,
|
||||
type Accessor,
|
||||
type ParentProps,
|
||||
type Setter,
|
||||
} from "solid-js"
|
||||
import { useTuiPaths } from "../../context/runtime"
|
||||
import { useProject } from "../../context/project"
|
||||
|
||||
export type HomeSessionDestination = { type: "directory"; directory: string; subdirectory: boolean } | { type: "new"; name: string }
|
||||
|
||||
type Context = {
|
||||
destination: Accessor<HomeSessionDestination | undefined>
|
||||
setDestination: Setter<HomeSessionDestination | undefined>
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
const HomeSessionDestinationContext = createContext<Context>()
|
||||
|
||||
export function HomeSessionDestinationProvider(props: ParentProps) {
|
||||
const project = useProject()
|
||||
const paths = useTuiPaths()
|
||||
const [selected, setDestination] = createSignal<HomeSessionDestination>()
|
||||
const destination = createMemo<HomeSessionDestination>(
|
||||
() => selected() ?? { type: "directory", directory: project.instance.directory() || paths.cwd, subdirectory: false },
|
||||
)
|
||||
return (
|
||||
<HomeSessionDestinationContext.Provider
|
||||
value={{ destination, setDestination, clear: () => setDestination(undefined) }}
|
||||
>
|
||||
{props.children}
|
||||
</HomeSessionDestinationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useHomeSessionDestination() {
|
||||
return useContext(HomeSessionDestinationContext)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ import { collapseToolOutput } from "../../util/collapse-tool-output"
|
||||
import { usePluginRuntime } from "../../plugin/runtime"
|
||||
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut } from "../../keymap"
|
||||
import { usePathFormatter } from "../../context/path-format"
|
||||
import { LocationProvider } from "../../context/location"
|
||||
import { useSetLocation } from "../../context/location"
|
||||
import { createSessionRows, resolvePart, type PartRef, type SessionRow } from "./rows"
|
||||
import { switchLabel } from "../../util/model"
|
||||
|
||||
@@ -115,6 +115,9 @@ export function Session() {
|
||||
const session = createMemo(() => data.session.get(route.sessionID))
|
||||
const messages = () => data.session.message.list(route.sessionID)
|
||||
const location = createMemo(() => session()?.location)
|
||||
const setLocation = useSetLocation()
|
||||
|
||||
createEffect(() => setLocation(location()))
|
||||
|
||||
createEffect(() => {
|
||||
const title = Locale.truncate(session()?.title ?? "", 50)
|
||||
@@ -823,7 +826,6 @@ export function Session() {
|
||||
)
|
||||
|
||||
return (
|
||||
<LocationProvider location={location()}>
|
||||
<context.Provider
|
||||
value={{
|
||||
get width() {
|
||||
@@ -949,7 +951,6 @@ export function Session() {
|
||||
</Show>
|
||||
</box>
|
||||
</context.Provider>
|
||||
</LocationProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2500,9 +2501,7 @@ function Subagent(props: ToolProps) {
|
||||
const { navigate } = useRoute()
|
||||
const data = useData()
|
||||
const input = createMemo(() => (typeof props.part.state.input === "string" ? {} : props.part.state.input))
|
||||
const metadata = createMemo(() =>
|
||||
props.part.state.status === "streaming" ? {} : props.part.state.structured,
|
||||
)
|
||||
const metadata = createMemo(() => (props.part.state.status === "streaming" ? {} : props.part.state.structured))
|
||||
const sessionID = createMemo(() => stringValue(metadata().sessionID) ?? stringValue(metadata().sessionId))
|
||||
const description = createMemo(() => stringValue(input().description))
|
||||
const isRunning = createMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user