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