Przeglądaj źródła

fix(tui): scope file autocomplete to session (#33458)

Dax 1 miesiąc temu
rodzic
commit
ef2357915e

+ 1 - 1
.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' }}
 

+ 7 - 4
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) {
                                                           <PromptHistoryProvider>
                                                             <PromptRefProvider>
                                                               <EditorContextProvider>
-                                                                <App
-                                                                  onSnapshot={input.onSnapshot}
-                                                                  pluginHost={input.pluginHost}
-                                                                />
+                                                                <LocationProvider>
+                                                                  <App
+                                                                    onSnapshot={input.onSnapshot}
+                                                                    pluginHost={input.pluginHost}
+                                                                  />
+                                                                </LocationProvider>
                                                               </EditorContextProvider>
                                                             </PromptRefProvider>
                                                           </PromptHistoryProvider>

+ 33 - 18
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,

+ 14 - 0
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<Accessor<LocationRef | undefined>>()
+
+export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) {
+  return <context.Provider value={() => props.location}>{props.children}</context.Provider>
+}
+
+export function useLocation() {
+  const value = useContext(context)
+  if (!value) throw new Error("Location context must be used within a LocationProvider")
+  return value
+}

+ 7 - 23
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 (
-    <context.Provider
-      value={{
-        path: () => props.path || paths.cwd,
-        format: (input) => formatPath(input, props.path || paths.cwd, paths.home),
-      }}
-    >
-      {props.children}
-    </context.Provider>
-  )
-}
-
 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) {

+ 8 - 3
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 (
-    <PathFormatterProvider path={session()?.directory}>
+    <LocationProvider location={location()}>
       <context.Provider
         value={{
           get width() {
@@ -1338,7 +1343,7 @@ export function Session() {
           </Show>
         </box>
       </context.Provider>
-    </PathFormatterProvider>
+    </LocationProvider>
   )
 }