diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5776e1bf2..13a76c0a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: - name: Run unit tests timeout-minutes: 20 - run: bun turbo test --output-logs=errors-only --log-order=grouped --log-prefix=none + run: GITHUB_ACTIONS=false bun turbo test env: OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 17a9a554c..39aa35993 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -35,6 +35,7 @@ import { SDKProvider, useSDK } from "./context/sdk" import { StartupLoading } from "./component/startup-loading" import { SyncProvider, useSync } from "./context/sync" import { DataProvider } from "./context/data" +import { LocationProvider } from "./context/location" import { LocalProvider, useLocal } from "./context/local" import { DialogModel } from "./component/dialog-model" import { useConnected } from "./component/use-connected" @@ -303,10 +304,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { - + + + diff --git a/packages/tui/src/component/prompt/autocomplete.tsx b/packages/tui/src/component/prompt/autocomplete.tsx index f2916173d..2a16017a2 100644 --- a/packages/tui/src/component/prompt/autocomplete.tsx +++ b/packages/tui/src/component/prompt/autocomplete.tsx @@ -13,6 +13,7 @@ import { useData } from "../../context/data" import { getScrollAcceleration } from "../../util/scroll" import { useTuiPaths } from "../../context/runtime" import { useTuiConfig } from "../../config" +import { useLocation } from "../../context/location" import { useTheme, selectedForeground } from "../../context/theme" import { SplitBorder } from "../../ui/border" import { useTerminalDimensions } from "@opentui/solid" @@ -21,6 +22,7 @@ import type { PromptInfo } from "../../prompt/history" import { useFrecency } from "../../prompt/frecency" import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap" import { displayCharAt, mentionTriggerIndex } from "../../prompt/display" +import type { FileSystemEntry } from "@opencode-ai/sdk/v2" function removeLineRange(input: string) { const hashIndex = input.lastIndexOf("#") @@ -94,6 +96,7 @@ export function Autocomplete(props: { const frecency = useFrecency() const tuiConfig = useTuiConfig() const paths = useTuiPaths() + const location = useLocation() const [store, setStore] = createStore({ index: 0, selected: 0, @@ -236,16 +239,18 @@ export function Autocomplete(props: { } } - function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) { - const baseDir = (sync.path.directory || paths.cwd).replace(/\/+$/, "") - const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item) - const urlObj = pathToFileURL(fullPath) + function createFilePart( + item: FileSystemEntry, + filePath: string, + lineRange?: { startLine: number; endLine?: number }, + ) { + const urlObj = pathToFileURL(filePath) const filename = - lineRange && !item.endsWith("/") - ? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` - : item + lineRange && item.type !== "directory" + ? `${item.path}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` + : item.path - if (lineRange && !item.endsWith("/")) { + if (lineRange && item.type !== "directory") { urlObj.searchParams.set("start", String(lineRange.startLine)) if (lineRange.endLine !== undefined) { urlObj.searchParams.set("end", String(lineRange.endLine)) @@ -254,10 +259,9 @@ export function Autocomplete(props: { return { filename, - url: urlObj.href, part: { type: "file" as const, - mime: "text/plain", + mime: item.mime, filename, url: urlObj.href, source: { @@ -267,7 +271,7 @@ export function Autocomplete(props: { end: 0, value: "", }, - path: item, + path: item.path, }, }, } @@ -284,7 +288,7 @@ export function Autocomplete(props: { }) function normalizeMentionPath(filePath: string) { - const baseDir = sync.path.directory || paths.cwd + const baseDir = location()?.directory || sync.path.directory || paths.cwd const absolute = path.resolve(filePath) const relative = path.relative(baseDir, absolute) @@ -301,7 +305,11 @@ export function Autocomplete(props: { startLine: input.lineStart, endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined, } - const { filename, part } = createFilePart(item, lineRange) + const { filename, part } = createFilePart( + { path: item, type: "file", mime: "text/plain" }, + input.filePath, + lineRange, + ) const index = store.visible === "@" ? store.index : props.input().cursorOffset setStore("visible", false) @@ -310,17 +318,20 @@ export function Autocomplete(props: { } const [files] = createResource( - () => search(), - async (query) => { + () => ({ query: search(), location: location() }), + async (input) => { if (!store.visible || store.visible === "/") return [] if (referenceMatch()) return [] - const { lineRange, baseQuery } = extractLineRange(query ?? "") + const { lineRange, baseQuery } = extractLineRange(input.query ?? "") // Get files from SDK const result = await sdk.client.v2.fs.find({ query: baseQuery, limit: "20", - location: { workspace: project.workspace.current() }, + location: { + directory: input.location?.directory, + workspace: input.location?.workspaceID ?? project.workspace.current(), + }, }) const options: AutocompleteOption[] = [] @@ -331,7 +342,11 @@ export function Autocomplete(props: { const width = props.anchor().width - 4 options.push( ...result.data.data.map((item): AutocompleteOption => { - const { filename, url, part } = createFilePart(item.path, lineRange) + const { filename, part } = createFilePart( + item, + path.join(result.data.location.directory, item.path), + lineRange, + ) return { display: Locale.truncateMiddle(filename, width), value: filename, diff --git a/packages/tui/src/context/location.tsx b/packages/tui/src/context/location.tsx new file mode 100644 index 000000000..0f3fca135 --- /dev/null +++ b/packages/tui/src/context/location.tsx @@ -0,0 +1,14 @@ +import type { LocationRef } from "@opencode-ai/sdk/v2" +import { createContext, useContext, type Accessor, type ParentProps } from "solid-js" + +const context = createContext>() + +export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) { + return props.location}>{props.children} +} + +export function useLocation() { + const value = useContext(context) + if (!value) throw new Error("Location context must be used within a LocationProvider") + return value +} diff --git a/packages/tui/src/context/path-format.tsx b/packages/tui/src/context/path-format.tsx index 8cf77aab9..52b1bee89 100644 --- a/packages/tui/src/context/path-format.tsx +++ b/packages/tui/src/context/path-format.tsx @@ -1,31 +1,15 @@ import path from "path" -import { createContext, useContext, type ParentProps } from "solid-js" import { abbreviateHome } from "../runtime" +import { useLocation } from "./location" import { useTuiPaths } from "./runtime" -const context = createContext<{ - path: () => string - format: (input?: string) => string -}>() - -export function PathFormatterProvider(props: ParentProps<{ path: string | undefined }>) { - const paths = useTuiPaths() - return ( - props.path || paths.cwd, - format: (input) => formatPath(input, props.path || paths.cwd, paths.home), - }} - > - {props.children} - - ) -} - export function usePathFormatter() { - const value = useContext(context) - if (!value) throw new Error("PathFormatter context must be used within a PathFormatterProvider") - return value + const paths = useTuiPaths() + const location = useLocation() + return { + path: () => location()?.directory || paths.cwd, + format: (input?: string) => formatPath(input, location()?.directory || paths.cwd, paths.home), + } } function formatPath(input: string | undefined, base: string, home: string) { diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e3758374c..11d8b4765 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -80,7 +80,8 @@ import { usePluginRuntime } from "../../plugin/runtime" import { DialogRetryAction } from "../../component/dialog-retry-action" import { getRevertDiffFiles } from "../../util/revert-diff" import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap" -import { PathFormatterProvider, usePathFormatter } from "../../context/path-format" +import { usePathFormatter } from "../../context/path-format" +import { LocationProvider } from "../../context/location" addDefaultParsers(parsers.parsers) @@ -193,6 +194,10 @@ export function Session() { const { theme } = useTheme() const promptRef = usePromptRef() const session = createMemo(() => sync.session.get(route.sessionID)) + const location = createMemo(() => { + const current = session() + return current ? { directory: current.directory, workspaceID: current.workspaceID } : undefined + }) createEffect(() => { const title = Locale.truncate(session()?.title ?? "", 50) @@ -1138,7 +1143,7 @@ export function Session() { createEffect(on(() => route.sessionID, toBottom)) return ( - + - + ) }