refactor(tui): centralize location state
This commit is contained in:
+41
-43
@@ -42,14 +42,13 @@ import { DialogProvider, useDialog } from "./ui/dialog"
|
|||||||
import { DialogIntegration } from "./component/dialog-integration"
|
import { DialogIntegration } from "./component/dialog-integration"
|
||||||
import { ErrorComponent } from "./component/error-component"
|
import { ErrorComponent } from "./component/error-component"
|
||||||
import { PluginRouteMissing } from "./component/plugin-route-missing"
|
import { PluginRouteMissing } from "./component/plugin-route-missing"
|
||||||
import { ProjectProvider, useProject } from "./context/project"
|
|
||||||
import { EditorContextProvider } from "./context/editor"
|
import { EditorContextProvider } from "./context/editor"
|
||||||
import { useEvent } from "./context/event"
|
import { useEvent } from "./context/event"
|
||||||
import { ClientProvider, useClient } from "./context/client"
|
import { ClientProvider, useClient } from "./context/client"
|
||||||
import { StartupLoading } from "./component/startup-loading"
|
import { StartupLoading } from "./component/startup-loading"
|
||||||
import { Reconnecting } from "./component/reconnecting"
|
import { Reconnecting } from "./component/reconnecting"
|
||||||
import { DataProvider, useData } from "./context/data"
|
import { DataProvider, useData } from "./context/data"
|
||||||
import { LocationProvider } from "./context/location"
|
import { LocationProvider, useLocation } from "./context/location"
|
||||||
import { LocalProvider, useLocal } from "./context/local"
|
import { LocalProvider, useLocal } from "./context/local"
|
||||||
import { PermissionProvider } from "./context/permission"
|
import { PermissionProvider } from "./context/permission"
|
||||||
import { DialogModel } from "./component/dialog-model"
|
import { DialogModel } from "./component/dialog-model"
|
||||||
@@ -327,40 +326,38 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
|
|||||||
<PluginRuntimeProvider value={pluginRuntime}>
|
<PluginRuntimeProvider value={pluginRuntime}>
|
||||||
<ClientProvider api={api} reconnect={reconnect} reload={input.server.reload}>
|
<ClientProvider api={api} reconnect={reconnect} reload={input.server.reload}>
|
||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<ProjectProvider>
|
<DataProvider>
|
||||||
<DataProvider>
|
<LocationProvider>
|
||||||
<LocationProvider>
|
<ThemeProvider mode={mode}>
|
||||||
<ThemeProvider mode={mode}>
|
<LocalProvider>
|
||||||
<LocalProvider>
|
<PromptStashProvider>
|
||||||
<PromptStashProvider>
|
<DialogProvider>
|
||||||
<DialogProvider>
|
<FrecencyProvider>
|
||||||
<FrecencyProvider>
|
<PromptHistoryProvider>
|
||||||
<PromptHistoryProvider>
|
<PromptRefProvider>
|
||||||
<PromptRefProvider>
|
<EditorContextProvider>
|
||||||
<EditorContextProvider>
|
<PluginProvider packages={input.packages}>
|
||||||
<PluginProvider packages={input.packages}>
|
<App
|
||||||
<App
|
pair={
|
||||||
pair={
|
input.server.endpoint.auth
|
||||||
input.server.endpoint.auth
|
? input.server.endpoint.auth
|
||||||
? input.server.endpoint.auth
|
: {
|
||||||
: {
|
username: "opencode",
|
||||||
username: "opencode",
|
password: "",
|
||||||
password: "",
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</PluginProvider>
|
||||||
</PluginProvider>
|
</EditorContextProvider>
|
||||||
</EditorContextProvider>
|
</PromptRefProvider>
|
||||||
</PromptRefProvider>
|
</PromptHistoryProvider>
|
||||||
</PromptHistoryProvider>
|
</FrecencyProvider>
|
||||||
</FrecencyProvider>
|
</DialogProvider>
|
||||||
</DialogProvider>
|
</PromptStashProvider>
|
||||||
</PromptStashProvider>
|
</LocalProvider>
|
||||||
</LocalProvider>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
</LocationProvider>
|
||||||
</LocationProvider>
|
</DataProvider>
|
||||||
</DataProvider>
|
|
||||||
</ProjectProvider>
|
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
</ClientProvider>
|
</ClientProvider>
|
||||||
</PluginRuntimeProvider>
|
</PluginRuntimeProvider>
|
||||||
@@ -413,7 +410,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||||||
const themeState = useTheme()
|
const themeState = useTheme()
|
||||||
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
const { theme, mode, setMode, locked, lock, unlock } = themeState
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
const location = useLocation()
|
||||||
const exit = useExit()
|
const exit = useExit()
|
||||||
const promptRef = usePromptRef()
|
const promptRef = usePromptRef()
|
||||||
const pluginRuntime = usePluginRuntime()
|
const pluginRuntime = usePluginRuntime()
|
||||||
@@ -988,12 +985,12 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
event.on("tui.command.execute", (evt, { workspace }) => {
|
event.on("tui.command.execute", (evt, { workspace }) => {
|
||||||
if (workspace !== project.workspace.current()) return
|
if (workspace !== (location.current?.workspaceID ?? data.location.default().workspaceID)) return
|
||||||
keymap.dispatchCommand(evt.data.command)
|
keymap.dispatchCommand(evt.data.command)
|
||||||
})
|
})
|
||||||
|
|
||||||
event.on("tui.toast.show", (evt, { workspace }) => {
|
event.on("tui.toast.show", (evt, { workspace }) => {
|
||||||
if (workspace !== project.workspace.current()) return
|
if (workspace !== (location.current?.workspaceID ?? data.location.default().workspaceID)) return
|
||||||
toast.show({
|
toast.show({
|
||||||
title: evt.data.title,
|
title: evt.data.title,
|
||||||
message: evt.data.message,
|
message: evt.data.message,
|
||||||
@@ -1003,13 +1000,14 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
event.on("plugin.updated", (_evt, { directory, workspace }) => {
|
event.on("plugin.updated", (_evt, { directory, workspace }) => {
|
||||||
if (directory !== project.instance.directory()) return
|
const current = location.current ?? data.location.default()
|
||||||
if (workspace !== project.workspace.current()) return
|
if (directory !== current.directory) return
|
||||||
|
if (workspace !== current.workspaceID) return
|
||||||
toast.show({ variant: "success", message: "Plugins reloaded" })
|
toast.show({ variant: "success", message: "Plugins reloaded" })
|
||||||
})
|
})
|
||||||
|
|
||||||
event.on("tui.session.select", (evt, { workspace }) => {
|
event.on("tui.session.select", (evt, { workspace }) => {
|
||||||
if (workspace !== project.workspace.current()) return
|
if (workspace !== (location.current?.workspaceID ?? data.location.default().workspaceID)) return
|
||||||
route.navigate({
|
route.navigate({
|
||||||
type: "session",
|
type: "session",
|
||||||
sessionID: evt.data.sessionID,
|
sessionID: evt.data.sessionID,
|
||||||
@@ -1027,7 +1025,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
event.on("session.error", (evt, { workspace }) => {
|
event.on("session.error", (evt, { workspace }) => {
|
||||||
if (workspace !== project.workspace.current()) return
|
if (workspace !== (location.current?.workspaceID ?? data.location.default().workspaceID)) return
|
||||||
const error = evt.data.error
|
const error = evt.data.error
|
||||||
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
||||||
const message = errorMessage(error)
|
const message = errorMessage(error)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { Locale } from "../util/locale"
|
|||||||
import { errorMessage } from "../util/error"
|
import { errorMessage } from "../util/error"
|
||||||
import { isRecord } from "../util/record"
|
import { isRecord } from "../util/record"
|
||||||
import { useToast } from "../ui/toast"
|
import { useToast } from "../ui/toast"
|
||||||
import { useProject } from "../context/project"
|
|
||||||
import { Spinner } from "./spinner"
|
import { Spinner } from "./spinner"
|
||||||
import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
|
import { DialogWorkspaceFileChanges } from "./dialog-workspace-file-changes"
|
||||||
import type { ProjectDirectoriesOutput } from "@opencode-ai/client"
|
import type { ProjectDirectoriesOutput } from "@opencode-ai/client"
|
||||||
@@ -41,11 +40,11 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const sessionData = useData()
|
const sessionData = useData()
|
||||||
const projectContext = useProject()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const shortcuts = Keymap.useShortcuts()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
|
const location = createMemo(() => sessionData.location.info())
|
||||||
const [working, setWorking] = createSignal(Boolean(props.initialRemoving))
|
const [working, setWorking] = createSignal(Boolean(props.initialRemoving))
|
||||||
const [toDelete, setToDelete] = createSignal<string>()
|
const [toDelete, setToDelete] = createSignal<string>()
|
||||||
const [removing, setRemoving] = createSignal(props.initialRemoving)
|
const [removing, setRemoving] = createSignal(props.initialRemoving)
|
||||||
@@ -63,15 +62,15 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
// swallow it and let the directory list render without a current marker.
|
// swallow it and let the directory list render without a current marker.
|
||||||
// Once the current project is known, a mismatch is a guaranteed miss.
|
// Once the current project is known, a mismatch is a guaranteed miss.
|
||||||
const [loadedProject] = createResource(
|
const [loadedProject] = createResource(
|
||||||
() => (projectContext.project() === undefined ? props.projectID : undefined),
|
() => (location()?.project.id === props.projectID ? undefined : props.projectID),
|
||||||
(projectID) =>
|
(projectID) =>
|
||||||
client.api.project
|
client.api.project
|
||||||
.current({ location: { directory: projectContext.instance.directory() || paths.cwd } })
|
.current({ location: { directory: location()?.directory || paths.cwd } })
|
||||||
.then((project) => (project.id === projectID ? project.directory : undefined))
|
.then((project) => (project.id === projectID ? project.directory : undefined))
|
||||||
.catch(() => undefined),
|
.catch(() => undefined),
|
||||||
)
|
)
|
||||||
const currentCheckout = createMemo(() => {
|
const currentCheckout = createMemo(() => {
|
||||||
if (projectContext.project() === props.projectID) return projectContext.instance.path().worktree
|
if (location()?.project.id === props.projectID) return location()?.project.directory
|
||||||
return loadedProject()
|
return loadedProject()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,14 +78,14 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
() => (props.initialRemoving ? undefined : props.projectID),
|
() => (props.initialRemoving ? undefined : props.projectID),
|
||||||
async (projectID, info): Promise<ReadonlyArray<ProjectDirectory> | undefined> => {
|
async (projectID, info): Promise<ReadonlyArray<ProjectDirectory> | undefined> => {
|
||||||
try {
|
try {
|
||||||
const location = { directory: projectContext.instance.directory() || paths.cwd }
|
const requestLocation = { directory: location()?.directory || paths.cwd }
|
||||||
await client.api.projectCopy.refresh({
|
await client.api.projectCopy.refresh({
|
||||||
projectID,
|
projectID,
|
||||||
location,
|
location: requestLocation,
|
||||||
})
|
})
|
||||||
const directories = await client.api.project.directories({
|
const directories = await client.api.project.directories({
|
||||||
projectID,
|
projectID,
|
||||||
location,
|
location: requestLocation,
|
||||||
})
|
})
|
||||||
setLoadError(undefined)
|
setLoadError(undefined)
|
||||||
return directories
|
return directories
|
||||||
@@ -204,7 +203,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
|
|
||||||
async function removedCurrent(current: boolean) {
|
async function removedCurrent(current: boolean) {
|
||||||
if (!current) return false
|
if (!current) return false
|
||||||
const fallback = projectContext.data.project.mainDir
|
const fallback = directoryData()?.findLast((item) => item.strategy === undefined)?.directory
|
||||||
if (fallback) setReplacementCurrent(fallback)
|
if (fallback) setReplacementCurrent(fallback)
|
||||||
if (route.data.type === "session") {
|
if (route.data.type === "session") {
|
||||||
route.navigate({ type: "home" })
|
route.navigate({ type: "home" })
|
||||||
@@ -236,7 +235,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
const error = await client.api.projectCopy
|
const error = await client.api.projectCopy
|
||||||
.remove({
|
.remove({
|
||||||
projectID: props.projectID,
|
projectID: props.projectID,
|
||||||
location: { directory: projectContext.instance.directory() || paths.cwd },
|
location: { directory: location()?.directory || paths.cwd },
|
||||||
directory: selected.directory,
|
directory: selected.directory,
|
||||||
force: false,
|
force: false,
|
||||||
})
|
})
|
||||||
@@ -263,7 +262,7 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
|||||||
const forcedError = await client.api.projectCopy
|
const forcedError = await client.api.projectCopy
|
||||||
.remove({
|
.remove({
|
||||||
projectID: props.projectID,
|
projectID: props.projectID,
|
||||||
location: { directory: projectContext.instance.directory() || paths.cwd },
|
location: { directory: location()?.directory || paths.cwd },
|
||||||
directory: selected.directory,
|
directory: selected.directory,
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useRoute } from "../context/route"
|
|||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { Locale } from "../util/locale"
|
import { Locale } from "../util/locale"
|
||||||
import { useProject } from "../context/project"
|
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
import { useClient } from "../context/client"
|
import { useClient } from "../context/client"
|
||||||
import { useLocal } from "../context/local"
|
import { useLocal } from "../context/local"
|
||||||
@@ -21,7 +20,6 @@ export function DialogSessionList() {
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
@@ -33,15 +31,16 @@ export function DialogSessionList() {
|
|||||||
|
|
||||||
const [searchResults] = createResource(search, async (query) => {
|
const [searchResults] = createResource(search, async (query) => {
|
||||||
if (!query) return
|
if (!query) return
|
||||||
const location = data.location.default()
|
|
||||||
try {
|
try {
|
||||||
|
if (!data.location.info()) await data.location.sync()
|
||||||
|
const current = data.location.info()
|
||||||
|
if (!current) throw new Error("Location unavailable")
|
||||||
const response = await client.api.session.list({
|
const response = await client.api.session.list({
|
||||||
|
project: current.project.id,
|
||||||
search: query,
|
search: query,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
parentID: null,
|
parentID: null,
|
||||||
directory: location.directory,
|
|
||||||
workspace: location.workspaceID,
|
|
||||||
})
|
})
|
||||||
return { query, sessions: response.data, error: undefined }
|
return { query, sessions: response.data, error: undefined }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -101,7 +100,8 @@ export function DialogSessionList() {
|
|||||||
|
|
||||||
const option = (session: SessionInfo, category: string) => {
|
const option = (session: SessionInfo, category: string) => {
|
||||||
const directory = session.location.directory
|
const directory = session.location.directory
|
||||||
const footer = directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
|
const footer =
|
||||||
|
directory !== data.location.info()?.project.directory ? Locale.truncate(path.basename(directory), 20) : ""
|
||||||
const slot = slotByID.get(session.id)
|
const slot = slotByID.get(session.id)
|
||||||
const deleting = toDelete() === session.id
|
const deleting = toDelete() === session.id
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { createMemo, createResource } from "solid-js"
|
import { createMemo, createResource } from "solid-js"
|
||||||
import { DialogSelect } from "../ui/dialog-select"
|
import { DialogSelect } from "../ui/dialog-select"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { useDialog } from "../ui/dialog"
|
||||||
import { useProject } from "../context/project"
|
|
||||||
import { useClient } from "../context/client"
|
import { useClient } from "../context/client"
|
||||||
|
import { useData } from "../context/data"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
|
|
||||||
export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const project = useProject()
|
const data = useData()
|
||||||
|
|
||||||
const [store] = createStore({
|
const [store] = createStore({
|
||||||
filter: "",
|
filter: "",
|
||||||
@@ -22,7 +22,10 @@ export function DialogTag(props: { onSelect?: (value: string) => void }) {
|
|||||||
query: store.filter,
|
query: store.filter,
|
||||||
type: "file",
|
type: "file",
|
||||||
limit: 5,
|
limit: 5,
|
||||||
location: { workspace: project.workspace.current() },
|
location: {
|
||||||
|
directory: data.location.default().directory,
|
||||||
|
workspace: data.location.default().workspaceID,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
return result?.data.map((item) => item.path) ?? []
|
return result?.data.map((item) => item.path) ?? []
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { firstBy } from "remeda"
|
|||||||
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
import { createMemo, createResource, createEffect, onMount, onCleanup, Index, Show, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { useEditorContext } from "../../context/editor"
|
import { useEditorContext } from "../../context/editor"
|
||||||
import { useProject } from "../../context/project"
|
|
||||||
import { useClient } from "../../context/client"
|
import { useClient } from "../../context/client"
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
@@ -87,7 +86,6 @@ export function Autocomplete(props: {
|
|||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
|
||||||
const slashes = useCommandSlashes()
|
const slashes = useCommandSlashes()
|
||||||
const keymap = Keymap.use()
|
const keymap = Keymap.use()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
@@ -284,7 +282,7 @@ export function Autocomplete(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function normalizeMentionPath(filePath: string) {
|
function normalizeMentionPath(filePath: string) {
|
||||||
const baseDir = location()?.directory || project.instance.directory() || paths.cwd
|
const baseDir = location.current?.directory || data.location.info()?.directory || paths.cwd
|
||||||
const absolute = path.resolve(filePath)
|
const absolute = path.resolve(filePath)
|
||||||
const relative = path.relative(baseDir, absolute)
|
const relative = path.relative(baseDir, absolute)
|
||||||
|
|
||||||
@@ -310,7 +308,7 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [files] = createResource(
|
const [files] = createResource(
|
||||||
() => ({ query: search(), location: location(), visible: store.visible }),
|
() => ({ query: search(), location: location.current, visible: store.visible }),
|
||||||
async (input) => {
|
async (input) => {
|
||||||
if (!input.visible || input.visible === "/") return { options: [], failed: false }
|
if (!input.visible || input.visible === "/") return { options: [], failed: false }
|
||||||
if (referenceMatch()) return { options: [], failed: false }
|
if (referenceMatch()) return { options: [], failed: false }
|
||||||
@@ -322,7 +320,7 @@ export function Autocomplete(props: {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
location: {
|
location: {
|
||||||
directory: input.location?.directory,
|
directory: input.location?.directory,
|
||||||
workspace: input.location?.workspaceID ?? project.workspace.current(),
|
workspace: input.location?.workspaceID ?? data.location.default().workspaceID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
@@ -365,7 +363,7 @@ export function Autocomplete(props: {
|
|||||||
const options: AutocompleteOption[] = []
|
const options: AutocompleteOption[] = []
|
||||||
const width = props.anchor().width - 4
|
const width = props.anchor().width - 4
|
||||||
|
|
||||||
for (const res of data.location.mcp.resource.list(location()) ?? []) {
|
for (const res of data.location.mcp.resource.list(location.current) ?? []) {
|
||||||
options.push({
|
options.push({
|
||||||
display: Locale.truncateMiddle(res.name, width),
|
display: Locale.truncateMiddle(res.name, width),
|
||||||
// Match the name only; matching the URI caused unrelated fuzzy hits.
|
// Match the name only; matching the URI caused unrelated fuzzy hits.
|
||||||
@@ -433,7 +431,7 @@ export function Autocomplete(props: {
|
|||||||
const results: AutocompleteOption[] = [...slashes()]
|
const results: AutocompleteOption[] = [...slashes()]
|
||||||
const commandNames = new Set<string>()
|
const commandNames = new Set<string>()
|
||||||
|
|
||||||
for (const serverCommand of data.location.command.list(location()) ?? []) {
|
for (const serverCommand of data.location.command.list(location.current) ?? []) {
|
||||||
commandNames.add(serverCommand.name)
|
commandNames.add(serverCommand.name)
|
||||||
results.push({
|
results.push({
|
||||||
display: "/" + serverCommand.name,
|
display: "/" + serverCommand.name,
|
||||||
@@ -449,7 +447,7 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const skill of data.location.skill
|
for (const skill of data.location.skill
|
||||||
.list(location())
|
.list(location.current)
|
||||||
?.filter((skill) => skill.slash === true && !commandNames.has(skill.id)) ?? []) {
|
?.filter((skill) => skill.slash === true && !commandNames.has(skill.id)) ?? []) {
|
||||||
results.push({
|
results.push({
|
||||||
display: "/" + skill.id,
|
display: "/" + skill.id,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { useClipboard } from "../../context/clipboard"
|
|||||||
import { Spinner } from "../spinner"
|
import { Spinner } from "../spinner"
|
||||||
import { useClient } from "../../context/client"
|
import { useClient } from "../../context/client"
|
||||||
import { useRoute } from "../../context/route"
|
import { useRoute } from "../../context/route"
|
||||||
import { useProject } from "../../context/project"
|
|
||||||
import { useEvent } from "../../context/event"
|
import { useEvent } from "../../context/event"
|
||||||
import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor"
|
import { editorSelectionKey, useEditorContext, type EditorSelection } from "../../context/editor"
|
||||||
import { normalizePromptContent, openEditor } from "../../editor"
|
import { normalizePromptContent, openEditor } from "../../editor"
|
||||||
@@ -151,7 +150,6 @@ export function Prompt(props: PromptProps) {
|
|||||||
const client = useClient()
|
const client = useClient()
|
||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const project = useProject()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const currentLocation = useLocation()
|
const currentLocation = useLocation()
|
||||||
const config = useConfig().data
|
const config = useConfig().data
|
||||||
@@ -165,7 +163,8 @@ export function Prompt(props: PromptProps) {
|
|||||||
.filter((id) => id !== props.sessionID && data.session.status(id) === "running").length
|
.filter((id) => id !== props.sessionID && data.session.status(id) === "running").length
|
||||||
})
|
})
|
||||||
const runningShells = createMemo(
|
const runningShells = createMemo(
|
||||||
() => data.shell.list(currentLocation()).filter((shell) => shell.metadata.sessionID === props.sessionID).length,
|
() =>
|
||||||
|
data.shell.list(currentLocation.current).filter((shell) => shell.metadata.sessionID === props.sessionID).length,
|
||||||
)
|
)
|
||||||
const history = usePromptHistory()
|
const history = usePromptHistory()
|
||||||
const stash = usePromptStash()
|
const stash = usePromptStash()
|
||||||
@@ -214,7 +213,8 @@ export function Prompt(props: PromptProps) {
|
|||||||
const editorContextLabelState = createMemo(() => editor.labelState())
|
const editorContextLabelState = createMemo(() => editor.labelState())
|
||||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||||
const move = usePromptMove({
|
const move = usePromptMove({
|
||||||
projectID: () => (props.sessionID ? data.session.get(props.sessionID)?.projectID : undefined) ?? project.project(),
|
projectID: () =>
|
||||||
|
(props.sessionID ? data.session.get(props.sessionID)?.projectID : undefined) ?? data.location.info()?.project.id,
|
||||||
sessionID: () => props.sessionID,
|
sessionID: () => props.sessionID,
|
||||||
})
|
})
|
||||||
const [cursorVersion, setCursorVersion] = createSignal(0)
|
const [cursorVersion, setCursorVersion] = createSignal(0)
|
||||||
@@ -244,7 +244,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
const event = useEvent()
|
const event = useEvent()
|
||||||
|
|
||||||
event.on("tui.prompt.append", (evt, { workspace }) => {
|
event.on("tui.prompt.append", (evt, { workspace }) => {
|
||||||
if (workspace !== project.workspace.current()) return
|
if (workspace !== (currentLocation.current?.workspaceID ?? data.location.default().workspaceID)) return
|
||||||
if (!input || input.isDestroyed) return
|
if (!input || input.isDestroyed) return
|
||||||
input.insertText(evt.data.text)
|
input.insertText(evt.data.text)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -465,8 +465,8 @@ export function Prompt(props: PromptProps) {
|
|||||||
renderer,
|
renderer,
|
||||||
value,
|
value,
|
||||||
cwd:
|
cwd:
|
||||||
(project.instance.path().worktree === "/" ? undefined : project.instance.path().worktree) ||
|
(data.location.info()?.project.directory === "/" ? undefined : data.location.info()?.project.directory) ||
|
||||||
project.instance.directory() ||
|
data.location.default().directory ||
|
||||||
paths.cwd,
|
paths.cwd,
|
||||||
})
|
})
|
||||||
if (!content) return
|
if (!content) return
|
||||||
@@ -502,7 +502,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
run: () => {
|
run: () => {
|
||||||
dialog.replace(() => (
|
dialog.replace(() => (
|
||||||
<DialogSkill
|
<DialogSkill
|
||||||
location={currentLocation()}
|
location={currentLocation.current}
|
||||||
onSelect={(skill) => {
|
onSelect={(skill) => {
|
||||||
input.setText(`/${skill} `)
|
input.setText(`/${skill} `)
|
||||||
setStore("prompt", {
|
setStore("prompt", {
|
||||||
@@ -1016,7 +1016,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
setStore("mode", "normal")
|
setStore("mode", "normal")
|
||||||
} else if (
|
} else if (
|
||||||
inputText.startsWith("/") &&
|
inputText.startsWith("/") &&
|
||||||
(data.location.command.list(currentLocation()) ?? []).some(
|
(data.location.command.list(currentLocation.current) ?? []).some(
|
||||||
(command) => command.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
(command) => command.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -1043,7 +1043,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
inputText.startsWith("/") &&
|
inputText.startsWith("/") &&
|
||||||
(data.location.skill.list(currentLocation()) ?? []).some(
|
(data.location.skill.list(currentLocation.current) ?? []).some(
|
||||||
(skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1),
|
(skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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 { useProject } from "../../context/project"
|
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
|
|
||||||
function moveReminderText(directory: string) {
|
function moveReminderText(directory: string) {
|
||||||
@@ -18,7 +17,6 @@ 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 project = useProject()
|
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const [creating, setCreating] = createSignal(false)
|
const [creating, setCreating] = createSignal(false)
|
||||||
@@ -34,7 +32,7 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
|||||||
try {
|
try {
|
||||||
const result = await client.api.projectCopy.create({
|
const result = await client.api.projectCopy.create({
|
||||||
projectID,
|
projectID,
|
||||||
location: { directory: project.instance.directory() || paths.cwd },
|
location: { directory: data.location.info()?.directory || paths.cwd },
|
||||||
strategy: "git_worktree",
|
strategy: "git_worktree",
|
||||||
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
directory: path.join(paths.worktree, projectID.slice(0, 6)),
|
||||||
name,
|
name,
|
||||||
@@ -77,8 +75,8 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: "directory",
|
type: "directory",
|
||||||
directory: project.instance.directory(),
|
directory: data.location.default().directory,
|
||||||
subdirectory: project.instance.directory() !== project.instance.path().worktree,
|
subdirectory: data.location.default().directory !== data.location.info()?.project.directory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onCurrentChange={setDestination}
|
onCurrentChange={setDestination}
|
||||||
@@ -130,8 +128,10 @@ export function usePromptMove(input: { projectID: () => string | undefined; sess
|
|||||||
if (projectID) return projectID
|
if (projectID) return projectID
|
||||||
const sessionID = input.sessionID()
|
const sessionID = input.sessionID()
|
||||||
if (sessionID) return (await resolveSession(sessionID))?.projectID
|
if (sessionID) return (await resolveSession(sessionID))?.projectID
|
||||||
|
const current = data.location.info()
|
||||||
|
if (current) return current.project.id
|
||||||
return client.api.project
|
return client.api.project
|
||||||
.current({ location: { directory: project.instance.directory() || paths.cwd } })
|
.current({ location: { directory: data.location.default().directory || paths.cwd } })
|
||||||
.then((project) => project.id)
|
.then((project) => project.id)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
FormInfo,
|
FormInfo,
|
||||||
IntegrationInfo,
|
IntegrationInfo,
|
||||||
LocationRef,
|
LocationRef,
|
||||||
|
LocationGetOutput,
|
||||||
McpResource,
|
McpResource,
|
||||||
McpServer,
|
McpServer,
|
||||||
ModelInfo,
|
ModelInfo,
|
||||||
@@ -43,6 +44,7 @@ const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_")
|
|||||||
export type FormWithLocation = FormInfo & { readonly location?: LocationRef }
|
export type FormWithLocation = FormInfo & { readonly location?: LocationRef }
|
||||||
|
|
||||||
type LocationData = {
|
type LocationData = {
|
||||||
|
info?: LocationGetOutput
|
||||||
agent?: AgentInfo[]
|
agent?: AgentInfo[]
|
||||||
command?: CommandInfo[]
|
command?: CommandInfo[]
|
||||||
integration?: IntegrationInfo[]
|
integration?: IntegrationInfo[]
|
||||||
@@ -1058,6 +1060,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
|
info(ref?: LocationRef) {
|
||||||
|
return store.location[locationKey(ref ?? defaultLocation())]?.info
|
||||||
|
},
|
||||||
default() {
|
default() {
|
||||||
return defaultLocation()
|
return defaultLocation()
|
||||||
},
|
},
|
||||||
@@ -1067,7 +1072,10 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
const location = await client.api.location.get({ location: locationQuery(current) })
|
const location = await client.api.location.get({ location: locationQuery(current) })
|
||||||
const key = locationKey(location)
|
const key = locationKey(location)
|
||||||
if (!store.location[key]) setStore("location", key, {})
|
if (!store.location[key]) setStore("location", key, {})
|
||||||
if (!ref) setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
setStore("location", key, "info", location)
|
||||||
|
if (!ref) {
|
||||||
|
setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const location = ref ?? defaultLocation()
|
const location = ref ?? defaultLocation()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -1275,12 +1283,16 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
void client.api.session
|
void client.api.location
|
||||||
.list({
|
.get({ location: locationQuery(defaultLocation()) })
|
||||||
limit: 50,
|
.then((location) => {
|
||||||
order: "desc",
|
const key = locationKey(location)
|
||||||
directory: defaultLocation().directory,
|
setStore("location", key, { ...store.location[key], info: location })
|
||||||
workspace: defaultLocation().workspaceID,
|
return client.api.session.list({
|
||||||
|
project: location.project.id,
|
||||||
|
limit: 50,
|
||||||
|
order: "desc",
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setStore(
|
setStore(
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createMemo } from "solid-js"
|
import { createMemo } from "solid-js"
|
||||||
import { useProject } from "./project"
|
import { useData } from "./data"
|
||||||
import { abbreviateHome } from "../runtime"
|
import { abbreviateHome } from "../runtime"
|
||||||
import { useTuiPaths } from "./runtime"
|
import { useTuiPaths } from "./runtime"
|
||||||
|
|
||||||
export function useDirectory() {
|
export function useDirectory() {
|
||||||
const project = useProject()
|
const data = useData()
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
return createMemo(() => {
|
return createMemo(() => {
|
||||||
const directory = project.instance.path().directory || paths.cwd
|
const directory = data.location.info()?.directory ?? data.location.default().directory ?? paths.cwd
|
||||||
return abbreviateHome(directory, paths.home)
|
return abbreviateHome(directory, paths.home)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { LocationRef } from "@opencode-ai/client"
|
import type { LocationGetOutput, LocationRef } from "@opencode-ai/client"
|
||||||
import { createContext, createSignal, onCleanup, useContext, type Accessor, type ParentProps } from "solid-js"
|
import { createContext, createMemo, createSignal, onCleanup, useContext, type ParentProps } from "solid-js"
|
||||||
import { useClient } from "./client"
|
import { useClient } from "./client"
|
||||||
import { useData } from "./data"
|
import { useData } from "./data"
|
||||||
|
|
||||||
const context = createContext<{
|
const context = createContext<{
|
||||||
current: Accessor<LocationRef | undefined>
|
readonly current: LocationGetOutput | undefined
|
||||||
set: (location?: LocationRef) => void
|
set: (location?: LocationRef) => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
export function LocationProvider(props: ParentProps) {
|
export function LocationProvider(props: ParentProps) {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const [current, setCurrent] = createSignal<LocationRef>()
|
const [ref, setRef] = createSignal<LocationRef>()
|
||||||
|
const current = createMemo(() => data.location.info(ref()))
|
||||||
|
|
||||||
function sync(location?: LocationRef) {
|
function sync(location?: LocationRef) {
|
||||||
if (!location) return
|
if (!location) return
|
||||||
@@ -24,23 +25,28 @@ export function LocationProvider(props: ParentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function set(location?: LocationRef) {
|
function set(location?: LocationRef) {
|
||||||
setCurrent(location)
|
setRef(location)
|
||||||
if (client.connection.status() === "connected") sync(location)
|
if (client.connection.status() === "connected") sync(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(client.event.on("server.connected", () => sync(current())))
|
onCleanup(client.event.on("server.connected", () => sync(ref())))
|
||||||
|
|
||||||
return <context.Provider value={{ current, set }}>{props.children}</context.Provider>
|
return (
|
||||||
|
<context.Provider
|
||||||
|
value={{
|
||||||
|
get current() {
|
||||||
|
return 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.current
|
return value
|
||||||
}
|
|
||||||
|
|
||||||
export function useSetLocation() {
|
|
||||||
const value = useContext(context)
|
|
||||||
if (!value) throw new Error("Location context must be used within a LocationProvider")
|
|
||||||
return value.set
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export function usePathFormatter() {
|
|||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
return {
|
return {
|
||||||
path: () => location()?.directory || paths.cwd,
|
path: () => location.current?.directory || paths.cwd,
|
||||||
format: (input?: string) => formatPath(input, location()?.directory || paths.cwd, paths.home),
|
format: (input?: string) => formatPath(input, location.current?.directory || paths.cwd, paths.home),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { batch } from "solid-js"
|
|
||||||
import { createStore, reconcile } from "solid-js/store"
|
|
||||||
import { createSimpleContext } from "./helper"
|
|
||||||
import { useClient } from "./client"
|
|
||||||
|
|
||||||
export const { use: useProject, provider: ProjectProvider } = createSimpleContext({
|
|
||||||
name: "Project",
|
|
||||||
init: () => {
|
|
||||||
const client = useClient()
|
|
||||||
|
|
||||||
const defaultPath = {
|
|
||||||
home: "",
|
|
||||||
state: "",
|
|
||||||
config: "",
|
|
||||||
worktree: "",
|
|
||||||
directory: process.cwd(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const [store, setStore] = createStore({
|
|
||||||
project: {
|
|
||||||
id: undefined as string | undefined,
|
|
||||||
worktree: undefined as string | undefined,
|
|
||||||
mainDir: undefined as string | undefined,
|
|
||||||
},
|
|
||||||
instance: {
|
|
||||||
path: defaultPath,
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
current: undefined as string | undefined,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function sync() {
|
|
||||||
const workspace = store.workspace.current
|
|
||||||
const location = { workspace }
|
|
||||||
const current = await client.api.location.get({ location })
|
|
||||||
const directories = await client.api.project.directories({ projectID: current.project.id, location })
|
|
||||||
batch(() => {
|
|
||||||
setStore(
|
|
||||||
"instance",
|
|
||||||
"path",
|
|
||||||
reconcile({ ...defaultPath, worktree: current.project.directory, directory: current.directory }),
|
|
||||||
)
|
|
||||||
setStore("project", "id", current.project.id)
|
|
||||||
setStore("project", "worktree", current.project.directory)
|
|
||||||
setStore("project", "mainDir", directories.findLast((item) => item.strategy === undefined)?.directory)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: store,
|
|
||||||
project() {
|
|
||||||
return store.project.id
|
|
||||||
},
|
|
||||||
instance: {
|
|
||||||
path() {
|
|
||||||
return store.instance.path
|
|
||||||
},
|
|
||||||
directory() {
|
|
||||||
return store.instance.path.directory
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
current() {
|
|
||||||
return store.workspace.current
|
|
||||||
},
|
|
||||||
set(next?: string | null) {
|
|
||||||
const workspace = next ?? undefined
|
|
||||||
if (store.workspace.current === workspace) return
|
|
||||||
setStore("workspace", "current", workspace)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
sync,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
import { Plugin } from "@opencode-ai/plugin/v2/tui"
|
||||||
|
import { createMemo, Show } from "solid-js"
|
||||||
|
import { useTuiPaths } from "../../context/runtime"
|
||||||
import { useTheme } from "../../context/theme"
|
import { useTheme } from "../../context/theme"
|
||||||
|
import { abbreviateHome } from "../../runtime"
|
||||||
|
import { FilePath } from "../../ui/file-path"
|
||||||
|
|
||||||
function View() {
|
function View(props: { context: Plugin.Context }) {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
return <text fg={theme.textMuted}>Sidebar footer unavailable</text>
|
const paths = useTuiPaths()
|
||||||
|
const directory = createMemo(() =>
|
||||||
|
props.context.location ? abbreviateHome(props.context.location.directory, paths.home) : undefined,
|
||||||
|
)
|
||||||
|
return <Show when={directory()}>{(value) => <FilePath value={value()} maxWidth={38} fg={theme.textMuted} />}</Show>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Plugin.define({
|
export default Plugin.define({
|
||||||
id: "opencode.sidebar-footer",
|
id: "opencode.sidebar-footer",
|
||||||
setup(context) {
|
setup(context) {
|
||||||
context.ui.slot("sidebar.footer", () => <View />)
|
context.ui.slot("sidebar.footer", () => <View context={context} />)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
|
|||||||
const context: Context = {
|
const context: Context = {
|
||||||
options: item.options ?? {},
|
options: item.options ?? {},
|
||||||
get location() {
|
get location() {
|
||||||
return location()
|
return location.current
|
||||||
},
|
},
|
||||||
client: client.api,
|
client: client.api,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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 { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { useSetLocation } from "../context/location"
|
import { useLocation } from "../context/location"
|
||||||
import { FormPrompt } from "./session/form"
|
import { FormPrompt } from "./session/form"
|
||||||
import { PluginSlot } from "../plugin/context"
|
import { PluginSlot } from "../plugin/context"
|
||||||
|
|
||||||
@@ -28,12 +28,12 @@ export function Home() {
|
|||||||
const local = useLocal()
|
const local = useLocal()
|
||||||
const editor = useEditorContext()
|
const editor = useEditorContext()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const setLocation = useSetLocation()
|
const location = useLocation()
|
||||||
// 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()))
|
createEffect(() => location.set(data.location.default()))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor.clearSelection()
|
editor.clearSelection()
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export function ShellTab(props: { sessionID: string }) {
|
|||||||
const shortcuts = Keymap.useShortcuts()
|
const shortcuts = Keymap.useShortcuts()
|
||||||
|
|
||||||
const entries = createMemo(() =>
|
const entries = createMemo(() =>
|
||||||
data.shell
|
data.shell.list().filter((shell) => shell.metadata.sessionID === props.sessionID && shell.status === "running"),
|
||||||
.list()
|
|
||||||
.filter((shell) => shell.metadata.sessionID === props.sessionID && shell.status === "running"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [store, setStore] = createStore({ selected: 0 })
|
const [store, setStore] = createStore({ selected: 0 })
|
||||||
@@ -47,8 +45,7 @@ export function ShellTab(props: { sessionID: string }) {
|
|||||||
const cleanup = composer.register({
|
const cleanup = composer.register({
|
||||||
id: "shell",
|
id: "shell",
|
||||||
label: "Shell",
|
label: "Shell",
|
||||||
hints: () =>
|
hints: () => (selectedEntry() ? [{ label: "kill", shortcut: shortcuts.get("composer.shell.kill") ?? "" }] : []),
|
||||||
selectedEntry() ? [{ label: "kill", shortcut: shortcuts.get("composer.shell.kill") ?? "" }] : [],
|
|
||||||
})
|
})
|
||||||
onCleanup(cleanup)
|
onCleanup(cleanup)
|
||||||
})
|
})
|
||||||
@@ -87,7 +84,7 @@ export function ShellTab(props: { sessionID: string }) {
|
|||||||
run() {
|
run() {
|
||||||
const entry = selectedEntry()
|
const entry = selectedEntry()
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
const ref = location()
|
const ref = location.current
|
||||||
void client.api.shell.remove({
|
void client.api.shell.remove({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
location: ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined,
|
location: ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined,
|
||||||
@@ -99,11 +96,7 @@ export function ShellTab(props: { sessionID: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={composer.active("shell")}>
|
<Show when={composer.active("shell")}>
|
||||||
<scrollbox
|
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
|
||||||
scrollbarOptions={{ visible: false }}
|
|
||||||
maxHeight={5}
|
|
||||||
ref={(r: ScrollBoxRenderable) => (scroll = r)}
|
|
||||||
>
|
|
||||||
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No shell commands</text>}>
|
<Show when={entries().length > 0} fallback={<text fg={theme.textMuted}> No shell commands</text>}>
|
||||||
<For each={entries()}>
|
<For each={entries()}>
|
||||||
{(shell, index) => {
|
{(shell, index) => {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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"
|
||||||
import { useProject } from "../../context/project"
|
|
||||||
import { useData } from "../../context/data"
|
import { useData } from "../../context/data"
|
||||||
import { SplitBorder } from "../../ui/border"
|
import { SplitBorder } from "../../ui/border"
|
||||||
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
import { useTuiPaths, useTuiTerminalEnvironment } from "../../context/runtime"
|
||||||
@@ -71,7 +70,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 { useSetLocation } from "../../context/location"
|
import { useLocation } 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"
|
||||||
|
|
||||||
@@ -106,7 +105,6 @@ export function Session() {
|
|||||||
const route = useRouteData("session")
|
const route = useRouteData("session")
|
||||||
const { navigate } = useRoute()
|
const { navigate } = useRoute()
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const project = useProject()
|
|
||||||
const paths = useTuiPaths()
|
const paths = useTuiPaths()
|
||||||
const configState = useConfig()
|
const configState = useConfig()
|
||||||
const config = configState.data
|
const config = configState.data
|
||||||
@@ -115,9 +113,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()
|
const currentLocation = useLocation()
|
||||||
|
|
||||||
createEffect(() => setLocation(location()))
|
createEffect(() => currentLocation.set(location()))
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const title = Locale.truncate(session()?.title ?? "", 50)
|
const title = Locale.truncate(session()?.title ?? "", 50)
|
||||||
@@ -211,7 +209,6 @@ export function Session() {
|
|||||||
navigate({ type: "home" })
|
navigate({ type: "home" })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
project.workspace.set(info.location.workspaceID)
|
|
||||||
editor.reconnect(info.location.directory)
|
editor.reconnect(info.location.directory)
|
||||||
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
if (route.sessionID === sessionID && scroll) scroll.scrollBy(100_000)
|
||||||
})().catch((error) => {
|
})().catch((error) => {
|
||||||
@@ -2348,9 +2345,7 @@ function Shell(props: ToolProps) {
|
|||||||
.output({
|
.output({
|
||||||
id,
|
id,
|
||||||
limit: 1024 * 1024,
|
limit: 1024 * 1024,
|
||||||
location: location
|
location: location ? { directory: location.directory, workspace: location.workspaceID } : undefined,
|
||||||
? { directory: location.directory, workspace: location.workspaceID }
|
|
||||||
: undefined,
|
|
||||||
})
|
})
|
||||||
.then((response) => setBackgroundOutput(stripAnsi(response.data.output.trim())))
|
.then((response) => setBackgroundOutput(stripAnsi(response.data.output.trim())))
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import type { OpenCodeEvent } from "@opencode-ai/client"
|
|||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { createEffect, onMount, type ParentProps } from "solid-js"
|
import { createEffect, onMount, type ParentProps } from "solid-js"
|
||||||
import { ProjectProvider } from "../../../src/context/project"
|
|
||||||
import { ClientProvider, useClient } from "../../../src/context/client"
|
import { ClientProvider, useClient } from "../../../src/context/client"
|
||||||
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
|
import { DataProvider as DataProviderBase, useData } from "../../../src/context/data"
|
||||||
import { LocationProvider, useSetLocation } from "../../../src/context/location"
|
import { LocationProvider, useLocation } from "../../../src/context/location"
|
||||||
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
|
||||||
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
|
import { createApi, createEventStream, createFetch, directory, json } from "../../fixture/tui-client"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
@@ -44,10 +43,14 @@ function DataProvider(props: ParentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProjectProvider(props: ParentProps) {
|
||||||
|
return props.children
|
||||||
|
}
|
||||||
|
|
||||||
function SyncLocation() {
|
function SyncLocation() {
|
||||||
const data = useData()
|
const data = useData()
|
||||||
const setLocation = useSetLocation()
|
const location = useLocation()
|
||||||
createEffect(() => setLocation(data.location.default()))
|
createEffect(() => location.set(data.location.default()))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { OpenCodeClient, OpenCodeEvent } from "@opencode-ai/client"
|
|||||||
import type { Service } from "@opencode-ai/client/effect"
|
import type { Service } from "@opencode-ai/client/effect"
|
||||||
import { testRender } from "@opentui/solid"
|
import { testRender } from "@opentui/solid"
|
||||||
import { onMount } from "solid-js"
|
import { onMount } from "solid-js"
|
||||||
import { ProjectProvider, useProject } from "../../../src/context/project"
|
|
||||||
import { ClientProvider, useClient } from "../../../src/context/client"
|
import { ClientProvider, useClient } from "../../../src/context/client"
|
||||||
import { useEvent } from "../../../src/context/event"
|
import { useEvent } from "../../../src/context/event"
|
||||||
import { createApi, createEventStream, createFetch } from "../../fixture/tui-client"
|
import { createApi, createEventStream, createFetch } from "../../fixture/tui-client"
|
||||||
@@ -61,7 +60,6 @@ async function mount(
|
|||||||
const calls = createFetch(undefined, events)
|
const calls = createFetch(undefined, events)
|
||||||
const seen: OpenCodeEvent[] = []
|
const seen: OpenCodeEvent[] = []
|
||||||
const workspaces: Array<string | undefined> = []
|
const workspaces: Array<string | undefined> = []
|
||||||
let project!: ReturnType<typeof useProject>
|
|
||||||
let client!: ReturnType<typeof useClient>
|
let client!: ReturnType<typeof useClient>
|
||||||
let done!: () => void
|
let done!: () => void
|
||||||
const ready = new Promise<void>((resolve) => {
|
const ready = new Promise<void>((resolve) => {
|
||||||
@@ -71,32 +69,27 @@ async function mount(
|
|||||||
const app = await testRender(() => (
|
const app = await testRender(() => (
|
||||||
<TestTuiContexts log={log}>
|
<TestTuiContexts log={log}>
|
||||||
<ClientProvider api={createApi(calls.fetch)} reconnect={reconnect}>
|
<ClientProvider api={createApi(calls.fetch)} reconnect={reconnect}>
|
||||||
<ProjectProvider>
|
<Probe
|
||||||
<Probe
|
onReady={(ctx) => {
|
||||||
onReady={async (ctx) => {
|
client = ctx.client
|
||||||
project = ctx.project
|
done()
|
||||||
client = ctx.client
|
}}
|
||||||
await project.sync()
|
seen={seen}
|
||||||
done()
|
workspaces={workspaces}
|
||||||
}}
|
/>
|
||||||
seen={seen}
|
|
||||||
workspaces={workspaces}
|
|
||||||
/>
|
|
||||||
</ProjectProvider>
|
|
||||||
</ClientProvider>
|
</ClientProvider>
|
||||||
</TestTuiContexts>
|
</TestTuiContexts>
|
||||||
))
|
))
|
||||||
|
|
||||||
await ready
|
await ready
|
||||||
return { app, events, emit: events.emit, project, client, seen, workspaces }
|
return { app, events, emit: events.emit, client, seen, workspaces }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Probe(props: {
|
function Probe(props: {
|
||||||
seen: OpenCodeEvent[]
|
seen: OpenCodeEvent[]
|
||||||
workspaces: Array<string | undefined>
|
workspaces: Array<string | undefined>
|
||||||
onReady: (ctx: { project: ReturnType<typeof useProject>; client: ReturnType<typeof useClient> }) => void
|
onReady: (ctx: { client: ReturnType<typeof useClient> }) => void
|
||||||
}) {
|
}) {
|
||||||
const project = useProject()
|
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
const event = useEvent()
|
const event = useEvent()
|
||||||
|
|
||||||
@@ -105,7 +98,7 @@ function Probe(props: {
|
|||||||
props.seen.push(evt)
|
props.seen.push(evt)
|
||||||
props.workspaces.push(workspace)
|
props.workspaces.push(workspace)
|
||||||
})
|
})
|
||||||
props.onReady({ project, client })
|
props.onReady({ client })
|
||||||
})
|
})
|
||||||
|
|
||||||
return <box />
|
return <box />
|
||||||
@@ -161,10 +154,9 @@ describe("useEvent", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("delivers current project events regardless of active workspace", async () => {
|
test("delivers current project events regardless of active workspace", async () => {
|
||||||
const { app, emit, project, seen } = await mount()
|
const { app, emit, seen } = await mount()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
project.workspace.set("ws_a")
|
|
||||||
emit(event(vcs("ws"), { directory: "/tmp/other", project: projectID, workspace: "ws_b" }))
|
emit(event(vcs("ws"), { directory: "/tmp/other", project: projectID, workspace: "ws_b" }))
|
||||||
|
|
||||||
await wait(() => seen.length === 1)
|
await wait(() => seen.length === 1)
|
||||||
@@ -176,10 +168,9 @@ describe("useEvent", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("delivers truly global events even when a workspace is active", async () => {
|
test("delivers truly global events even when a workspace is active", async () => {
|
||||||
const { app, emit, project, seen } = await mount()
|
const { app, emit, seen } = await mount()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
project.workspace.set("ws_a")
|
|
||||||
emit(event(update("1.2.3"), { directory: "global" }))
|
emit(event(update("1.2.3"), { directory: "global" }))
|
||||||
|
|
||||||
await wait(() => seen.length === 1)
|
await wait(() => seen.length === 1)
|
||||||
@@ -256,9 +247,7 @@ describe("useEvent", () => {
|
|||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
controller.enqueue(
|
controller.enqueue(encoder.encode('data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n'))
|
||||||
encoder.encode('data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n'),
|
|
||||||
)
|
|
||||||
controller.close()
|
controller.close()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user