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 (
-
+
-
+
)
}