Skip to content

Commit ef23579

Browse files
authored
fix(tui): scope file autocomplete to session (anomalyco#33458)
1 parent 975b113 commit ef23579

6 files changed

Lines changed: 70 additions & 49 deletions

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
6666
- name: Run unit tests
6767
timeout-minutes: 20
68-
run: bun turbo test --output-logs=errors-only --log-order=grouped --log-prefix=none
68+
run: GITHUB_ACTIONS=false bun turbo test
6969
env:
7070
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
7171

packages/tui/src/app.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { SDKProvider, useSDK } from "./context/sdk"
3535
import { StartupLoading } from "./component/startup-loading"
3636
import { SyncProvider, useSync } from "./context/sync"
3737
import { DataProvider } from "./context/data"
38+
import { LocationProvider } from "./context/location"
3839
import { LocalProvider, useLocal } from "./context/local"
3940
import { DialogModel } from "./component/dialog-model"
4041
import { useConnected } from "./component/use-connected"
@@ -303,10 +304,12 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
303304
<PromptHistoryProvider>
304305
<PromptRefProvider>
305306
<EditorContextProvider>
306-
<App
307-
onSnapshot={input.onSnapshot}
308-
pluginHost={input.pluginHost}
309-
/>
307+
<LocationProvider>
308+
<App
309+
onSnapshot={input.onSnapshot}
310+
pluginHost={input.pluginHost}
311+
/>
312+
</LocationProvider>
310313
</EditorContextProvider>
311314
</PromptRefProvider>
312315
</PromptHistoryProvider>

packages/tui/src/component/prompt/autocomplete.tsx

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useData } from "../../context/data"
1313
import { getScrollAcceleration } from "../../util/scroll"
1414
import { useTuiPaths } from "../../context/runtime"
1515
import { useTuiConfig } from "../../config"
16+
import { useLocation } from "../../context/location"
1617
import { useTheme, selectedForeground } from "../../context/theme"
1718
import { SplitBorder } from "../../ui/border"
1819
import { useTerminalDimensions } from "@opentui/solid"
@@ -21,6 +22,7 @@ import type { PromptInfo } from "../../prompt/history"
2122
import { useFrecency } from "../../prompt/frecency"
2223
import { useBindings, useCommandSlashes, useOpencodeModeStack } from "../../keymap"
2324
import { displayCharAt, mentionTriggerIndex } from "../../prompt/display"
25+
import type { FileSystemEntry } from "@opencode-ai/sdk/v2"
2426

2527
function removeLineRange(input: string) {
2628
const hashIndex = input.lastIndexOf("#")
@@ -94,6 +96,7 @@ export function Autocomplete(props: {
9496
const frecency = useFrecency()
9597
const tuiConfig = useTuiConfig()
9698
const paths = useTuiPaths()
99+
const location = useLocation()
97100
const [store, setStore] = createStore({
98101
index: 0,
99102
selected: 0,
@@ -236,16 +239,18 @@ export function Autocomplete(props: {
236239
}
237240
}
238241

239-
function createFilePart(item: string, lineRange?: { startLine: number; endLine?: number }) {
240-
const baseDir = (sync.path.directory || paths.cwd).replace(/\/+$/, "")
241-
const fullPath = path.isAbsolute(item) ? item : path.join(baseDir, item)
242-
const urlObj = pathToFileURL(fullPath)
242+
function createFilePart(
243+
item: FileSystemEntry,
244+
filePath: string,
245+
lineRange?: { startLine: number; endLine?: number },
246+
) {
247+
const urlObj = pathToFileURL(filePath)
243248
const filename =
244-
lineRange && !item.endsWith("/")
245-
? `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
246-
: item
249+
lineRange && item.type !== "directory"
250+
? `${item.path}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}`
251+
: item.path
247252

248-
if (lineRange && !item.endsWith("/")) {
253+
if (lineRange && item.type !== "directory") {
249254
urlObj.searchParams.set("start", String(lineRange.startLine))
250255
if (lineRange.endLine !== undefined) {
251256
urlObj.searchParams.set("end", String(lineRange.endLine))
@@ -254,10 +259,9 @@ export function Autocomplete(props: {
254259

255260
return {
256261
filename,
257-
url: urlObj.href,
258262
part: {
259263
type: "file" as const,
260-
mime: "text/plain",
264+
mime: item.mime,
261265
filename,
262266
url: urlObj.href,
263267
source: {
@@ -267,7 +271,7 @@ export function Autocomplete(props: {
267271
end: 0,
268272
value: "",
269273
},
270-
path: item,
274+
path: item.path,
271275
},
272276
},
273277
}
@@ -284,7 +288,7 @@ export function Autocomplete(props: {
284288
})
285289

286290
function normalizeMentionPath(filePath: string) {
287-
const baseDir = sync.path.directory || paths.cwd
291+
const baseDir = location()?.directory || sync.path.directory || paths.cwd
288292
const absolute = path.resolve(filePath)
289293
const relative = path.relative(baseDir, absolute)
290294

@@ -301,7 +305,11 @@ export function Autocomplete(props: {
301305
startLine: input.lineStart,
302306
endLine: input.lineEnd > input.lineStart ? input.lineEnd : undefined,
303307
}
304-
const { filename, part } = createFilePart(item, lineRange)
308+
const { filename, part } = createFilePart(
309+
{ path: item, type: "file", mime: "text/plain" },
310+
input.filePath,
311+
lineRange,
312+
)
305313
const index = store.visible === "@" ? store.index : props.input().cursorOffset
306314

307315
setStore("visible", false)
@@ -310,17 +318,20 @@ export function Autocomplete(props: {
310318
}
311319

312320
const [files] = createResource(
313-
() => search(),
314-
async (query) => {
321+
() => ({ query: search(), location: location() }),
322+
async (input) => {
315323
if (!store.visible || store.visible === "/") return []
316324
if (referenceMatch()) return []
317-
const { lineRange, baseQuery } = extractLineRange(query ?? "")
325+
const { lineRange, baseQuery } = extractLineRange(input.query ?? "")
318326

319327
// Get files from SDK
320328
const result = await sdk.client.v2.fs.find({
321329
query: baseQuery,
322330
limit: "20",
323-
location: { workspace: project.workspace.current() },
331+
location: {
332+
directory: input.location?.directory,
333+
workspace: input.location?.workspaceID ?? project.workspace.current(),
334+
},
324335
})
325336

326337
const options: AutocompleteOption[] = []
@@ -331,7 +342,11 @@ export function Autocomplete(props: {
331342
const width = props.anchor().width - 4
332343
options.push(
333344
...result.data.data.map((item): AutocompleteOption => {
334-
const { filename, url, part } = createFilePart(item.path, lineRange)
345+
const { filename, part } = createFilePart(
346+
item,
347+
path.join(result.data.location.directory, item.path),
348+
lineRange,
349+
)
335350
return {
336351
display: Locale.truncateMiddle(filename, width),
337352
value: filename,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { LocationRef } from "@opencode-ai/sdk/v2"
2+
import { createContext, useContext, type Accessor, type ParentProps } from "solid-js"
3+
4+
const context = createContext<Accessor<LocationRef | undefined>>()
5+
6+
export function LocationProvider(props: ParentProps<{ location?: LocationRef }>) {
7+
return <context.Provider value={() => props.location}>{props.children}</context.Provider>
8+
}
9+
10+
export function useLocation() {
11+
const value = useContext(context)
12+
if (!value) throw new Error("Location context must be used within a LocationProvider")
13+
return value
14+
}

packages/tui/src/context/path-format.tsx

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,15 @@
11
import path from "path"
2-
import { createContext, useContext, type ParentProps } from "solid-js"
32
import { abbreviateHome } from "../runtime"
3+
import { useLocation } from "./location"
44
import { useTuiPaths } from "./runtime"
55

6-
const context = createContext<{
7-
path: () => string
8-
format: (input?: string) => string
9-
}>()
10-
11-
export function PathFormatterProvider(props: ParentProps<{ path: string | undefined }>) {
12-
const paths = useTuiPaths()
13-
return (
14-
<context.Provider
15-
value={{
16-
path: () => props.path || paths.cwd,
17-
format: (input) => formatPath(input, props.path || paths.cwd, paths.home),
18-
}}
19-
>
20-
{props.children}
21-
</context.Provider>
22-
)
23-
}
24-
256
export function usePathFormatter() {
26-
const value = useContext(context)
27-
if (!value) throw new Error("PathFormatter context must be used within a PathFormatterProvider")
28-
return value
7+
const paths = useTuiPaths()
8+
const location = useLocation()
9+
return {
10+
path: () => location()?.directory || paths.cwd,
11+
format: (input?: string) => formatPath(input, location()?.directory || paths.cwd, paths.home),
12+
}
2913
}
3014

3115
function formatPath(input: string | undefined, base: string, home: string) {

packages/tui/src/routes/session/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ import { usePluginRuntime } from "../../plugin/runtime"
8080
import { DialogRetryAction } from "../../component/dialog-retry-action"
8181
import { getRevertDiffFiles } from "../../util/revert-diff"
8282
import { OPENCODE_BASE_MODE, useBindings, useCommandShortcut, useOpencodeKeymap } from "../../keymap"
83-
import { PathFormatterProvider, usePathFormatter } from "../../context/path-format"
83+
import { usePathFormatter } from "../../context/path-format"
84+
import { LocationProvider } from "../../context/location"
8485

8586
addDefaultParsers(parsers.parsers)
8687

@@ -193,6 +194,10 @@ export function Session() {
193194
const { theme } = useTheme()
194195
const promptRef = usePromptRef()
195196
const session = createMemo(() => sync.session.get(route.sessionID))
197+
const location = createMemo(() => {
198+
const current = session()
199+
return current ? { directory: current.directory, workspaceID: current.workspaceID } : undefined
200+
})
196201

197202
createEffect(() => {
198203
const title = Locale.truncate(session()?.title ?? "", 50)
@@ -1138,7 +1143,7 @@ export function Session() {
11381143
createEffect(on(() => route.sessionID, toBottom))
11391144

11401145
return (
1141-
<PathFormatterProvider path={session()?.directory}>
1146+
<LocationProvider location={location()}>
11421147
<context.Provider
11431148
value={{
11441149
get width() {
@@ -1338,7 +1343,7 @@ export function Session() {
13381343
</Show>
13391344
</box>
13401345
</context.Provider>
1341-
</PathFormatterProvider>
1346+
</LocationProvider>
13421347
)
13431348
}
13441349

0 commit comments

Comments
 (0)