diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index e22423309ae..2af5b21152c 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -23,6 +23,7 @@ import { ThemeProvider, useTheme } from "@tui/context/theme"
import { Home } from "@tui/routes/home"
import { Session } from "@tui/routes/session"
import { PromptHistoryProvider } from "./component/prompt/history"
+import { FrecencyProvider } from "./component/prompt/frecency"
import { PromptStashProvider } from "./component/prompt/stash"
import { DialogAlert } from "./ui/dialog-alert"
import { ToastProvider, useToast } from "./ui/toast"
@@ -124,11 +125,13 @@ export function tui(input: { url: string; args: Args; directory?: string; onExit
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
index ae4f18d4cb3..8a6db34242a 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx
@@ -11,6 +11,7 @@ import { useCommandDialog } from "@tui/component/dialog-command"
import { useTerminalDimensions } from "@opentui/solid"
import { Locale } from "@/util/locale"
import type { PromptInfo } from "./history"
+import { useFrecency } from "./frecency"
function removeLineRange(input: string) {
const hashIndex = input.lastIndexOf("#")
@@ -57,6 +58,7 @@ export type AutocompleteOption = {
description?: string
isDirectory?: boolean
onSelect?: () => void
+ path?: string
}
export function Autocomplete(props: {
@@ -76,6 +78,7 @@ export function Autocomplete(props: {
const command = useCommandDialog()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()
+ const frecency = useFrecency()
const [store, setStore] = createStore({
index: 0,
@@ -168,6 +171,10 @@ export function Autocomplete(props: {
draft.parts.push(part)
props.setExtmark(partIndex, extmarkId)
})
+
+ if (part.type === "file" && part.source && part.source.type === "file") {
+ frecency.updateFrecency(part.source.path)
+ }
}
const [files] = createResource(
@@ -186,9 +193,19 @@ export function Autocomplete(props: {
// Add file options
if (!result.error && result.data) {
+ const sortedFiles = result.data.sort((a, b) => {
+ const aScore = frecency.getFrecency(a)
+ const bScore = frecency.getFrecency(b)
+ if (aScore !== bScore) return bScore - aScore
+ const aDepth = a.split("/").length
+ const bDepth = b.split("/").length
+ if (aDepth !== bDepth) return aDepth - bDepth
+ return a.localeCompare(b)
+ })
+
const width = props.anchor().width - 4
options.push(
- ...result.data.map((item): AutocompleteOption => {
+ ...sortedFiles.map((item): AutocompleteOption => {
let url = `file://${process.cwd()}/${item}`
let filename = item
if (lineRange && !item.endsWith("/")) {
@@ -205,6 +222,7 @@ export function Autocomplete(props: {
return {
display: Locale.truncateMiddle(filename, width),
isDirectory: isDir,
+ path: item,
onSelect: () => {
insertPart(filename, {
type: "file",
@@ -471,10 +489,12 @@ export function Autocomplete(props: {
limit: 10,
scoreFn: (objResults) => {
const displayResult = objResults[0]
+ let score = objResults.score
if (displayResult && displayResult.target.startsWith(store.visible + currentFilter)) {
- return objResults.score * 2
+ score *= 2
}
- return objResults.score
+ const frecencyScore = objResults.obj.path ? frecency.getFrecency(objResults.obj.path) : 0
+ return score * (1 + frecencyScore)
},
})
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx
new file mode 100644
index 00000000000..5f8a3920d53
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx
@@ -0,0 +1,89 @@
+import path from "path"
+import { Global } from "@/global"
+import { onMount } from "solid-js"
+import { createStore } from "solid-js/store"
+import { createSimpleContext } from "../../context/helper"
+import { appendFile } from "fs/promises"
+
+function calculateFrecency(entry?: { frequency: number; lastOpen: number }): number {
+ if (!entry) return 0
+ const daysSince = (Date.now() - entry.lastOpen) / 86400000 // ms per day
+ const weight = 1 / (1 + daysSince)
+ return entry.frequency * weight
+}
+
+const MAX_FRECENCY_ENTRIES = 1000
+
+export const { use: useFrecency, provider: FrecencyProvider } = createSimpleContext({
+ name: "Frecency",
+ init: () => {
+ const frecencyFile = Bun.file(path.join(Global.Path.state, "frecency.jsonl"))
+ onMount(async () => {
+ const text = await frecencyFile.text().catch(() => "")
+ const lines = text
+ .split("\n")
+ .filter(Boolean)
+ .map((line) => {
+ try {
+ return JSON.parse(line) as { path: string; frequency: number; lastOpen: number }
+ } catch {
+ return null
+ }
+ })
+ .filter((line): line is { path: string; frequency: number; lastOpen: number } => line !== null)
+
+ const latest = lines.reduce(
+ (acc, entry) => {
+ acc[entry.path] = entry
+ return acc
+ },
+ {} as Record,
+ )
+
+ const sorted = Object.values(latest)
+ .sort((a, b) => b.lastOpen - a.lastOpen)
+ .slice(0, MAX_FRECENCY_ENTRIES)
+
+ setStore(
+ "data",
+ Object.fromEntries(
+ sorted.map((entry) => [entry.path, { frequency: entry.frequency, lastOpen: entry.lastOpen }]),
+ ),
+ )
+
+ if (sorted.length > 0) {
+ const content = sorted.map((entry) => JSON.stringify(entry)).join("\n") + "\n"
+ Bun.write(frecencyFile, content).catch(() => {})
+ }
+ })
+
+ const [store, setStore] = createStore({
+ data: {} as Record,
+ })
+
+ function updateFrecency(filePath: string) {
+ const absolutePath = path.resolve(process.cwd(), filePath)
+ const newEntry = {
+ frequency: (store.data[absolutePath]?.frequency || 0) + 1,
+ lastOpen: Date.now(),
+ }
+ setStore("data", absolutePath, newEntry)
+ appendFile(frecencyFile.name!, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {})
+
+ if (Object.keys(store.data).length > MAX_FRECENCY_ENTRIES) {
+ const sorted = Object.entries(store.data)
+ .sort(([, a], [, b]) => b.lastOpen - a.lastOpen)
+ .slice(0, MAX_FRECENCY_ENTRIES)
+ setStore("data", Object.fromEntries(sorted))
+ const content = sorted.map(([path, entry]) => JSON.stringify({ path, ...entry })).join("\n") + "\n"
+ Bun.write(frecencyFile, content).catch(() => {})
+ }
+ }
+
+ return {
+ getFrecency: (filePath: string) => calculateFrecency(store.data[path.resolve(process.cwd(), filePath)]),
+ updateFrecency,
+ data: () => store.data,
+ }
+ },
+})