Skip to content

Commit dc7d229

Browse files
committed
app: add manual refresh for the session file tree
invalidateFromWatcher() only refreshes the parent directory of a changed path and only when it is already loaded, so external changes can leave the tree stale until a full page reload. Add file.tree.refreshAll() (force-reload every listed directory) and a refresh button in the sidebar header.
1 parent 830d5eb commit dc7d229

67 files changed

Lines changed: 177 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/app/src/context/file.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({
275275
tree: {
276276
list: tree.listDir,
277277
refresh: (input: string) => tree.listDir(input, { force: true }),
278+
refreshAll: () => tree.refreshAll(),
278279
state: tree.dirState,
279280
children: tree.children,
280281
expand: tree.expandDir,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { createFileTreeStore } from "./tree-store"
3+
import type { FileNode } from "@opencode-ai/sdk/v2"
4+
5+
function node(path: string, type: FileNode["type"] = "file"): FileNode {
6+
return { path, type, name: path.split("/").pop() ?? path, absolute: `/repo/${path}`, ignored: false }
7+
}
8+
9+
function makeStore(listing: Record<string, FileNode[]>) {
10+
const calls: string[] = []
11+
const store = createFileTreeStore({
12+
scope: () => "",
13+
normalizeDir: (input) => input,
14+
list: (dir) => {
15+
calls.push(dir)
16+
return Promise.resolve(listing[dir] ?? [])
17+
},
18+
onError: () => {},
19+
})
20+
return { store, calls }
21+
}
22+
23+
describe("file tree store", () => {
24+
test("lists a directory once and caches it", async () => {
25+
const listing: Record<string, FileNode[]> = { "": [node("src", "directory")] }
26+
const { store, calls } = makeStore(listing)
27+
28+
await store.listDir("")
29+
await store.listDir("")
30+
31+
expect(calls).toEqual([""])
32+
expect(store.children("")).toHaveLength(1)
33+
expect(store.loadedDirs()).toEqual([""])
34+
})
35+
36+
test("refreshAll force-reloads loaded directories and picks up new entries", async () => {
37+
const listing: Record<string, FileNode[]> = {
38+
"": [node("src", "directory")],
39+
src: [node("src/a.ts")],
40+
}
41+
const { store, calls } = makeStore(listing)
42+
43+
await store.listDir("")
44+
await store.listDir("src")
45+
expect(store.children("src")).toHaveLength(1)
46+
47+
listing.src = [node("src/a.ts"), node("src/b.ts")]
48+
await store.refreshAll()
49+
50+
expect(store.children("src")).toHaveLength(2)
51+
expect(calls).toEqual(["", "src", "", "src"])
52+
})
53+
54+
test("refreshAll skips directories that were never loaded", async () => {
55+
const listing: Record<string, FileNode[]> = {
56+
"": [node("src", "directory")],
57+
src: [node("src/a.ts")],
58+
}
59+
const { store, calls } = makeStore(listing)
60+
61+
await store.listDir("")
62+
await store.refreshAll()
63+
64+
expect(calls).toEqual(["", ""])
65+
expect(store.children("src")).toEqual([])
66+
})
67+
68+
test("refreshAll resolves when nothing is loaded", async () => {
69+
const { store, calls } = makeStore({})
70+
await store.refreshAll()
71+
expect(calls).toEqual([])
72+
})
73+
})

packages/app/src/context/file/tree-store.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ export function createFileTreeStore(options: TreeStoreOptions) {
144144
setTree("dir", dir, "expanded", false)
145145
}
146146

147+
const loadedDirs = () => Object.keys(tree.dir).filter((key) => tree.dir[key]?.loaded)
148+
149+
// Force-reload every directory that has been listed at least once. Used by the
150+
// manual tree refresh: the watcher only invalidates directories it knows about,
151+
// so an external change can otherwise go unseen until a full page reload.
152+
const refreshAll = async () => {
153+
await Promise.all(loadedDirs().map((dir) => listDir(dir, { force: true })))
154+
}
155+
147156
const dirState = (input: string) => {
148157
const dir = options.normalizeDir(input)
149158
return tree.dir[dir]
@@ -165,6 +174,8 @@ export function createFileTreeStore(options: TreeStoreOptions) {
165174
listDir,
166175
expandDir,
167176
collapseDir,
177+
loadedDirs,
178+
refreshAll,
168179
dirState,
169180
children,
170181
node: (path: string) => tree.node[path],

packages/app/src/i18n/am.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ export const dict = {
711711
"session.review.noChanges": "ምንም ለውጦች የሉም",
712712
"session.review.noUncommittedChanges": "ገና ምንም ያልተደረጉ ለውጦች የሉም",
713713
"session.review.noBranchChanges": "ገና ምንም ቅርንጫፍ ምንም ለውጥ የለም",
714+
"session.files.refresh": "አድስ",
714715
"session.files.selectToOpen": "ለመክፈት ፋይል ምረጥ",
715716
"session.files.all": "ሁሉም ፋይሎች",
716717
"session.files.empty": "ምንም ፋይሎች",

packages/app/src/i18n/ar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ export const dict = {
706706
"session.review.noBranchChanges": "لا توجد تغييرات في الفرع بعد",
707707
"session.review.noVcs": "لم يُكتشف نظام Git للتحكم في الإصدارات، لذا لن تُعرض التغييرات",
708708
"session.review.noSnapshot": "تم تعطيل تتبع اللقطات في التكوين، لذا فإن تغييرات الجلسة غير متوفرة",
709+
"session.files.refresh": "تحديث",
709710
"session.files.selectToOpen": "اختر ملفًا لفتحه",
710711
"session.files.all": "كل الملفات",
711712
"session.files.empty": "لا توجد ملفات",

packages/app/src/i18n/az.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ export const dict = {
734734
"session.review.noChanges": "Dəyişiklik yoxdur",
735735
"session.review.noUncommittedChanges": "Hələ commit edilməmiş dəyişiklik yoxdur",
736736
"session.review.noBranchChanges": "Hələ branch dəyişikliyi yoxdur",
737+
"session.files.refresh": "Yeniləyin",
737738
"session.files.selectToOpen": "Açmaq üçün fayl seçin",
738739
"session.files.all": "Bütün fayllar",
739740
"session.files.empty": "Fayl yoxdur",

packages/app/src/i18n/bg.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ export const dict = {
730730
"session.review.noChanges": "Без промени",
731731
"session.review.noUncommittedChanges": "Все още няма незавършени промени",
732732
"session.review.noBranchChanges": "Все още няма промени в клона",
733+
"session.files.refresh": "Опресняване",
733734
"session.files.selectToOpen": "Изберете файл за отваряне",
734735
"session.files.all": "Всички файлове",
735736
"session.files.empty": "Няма файлове",

packages/app/src/i18n/bn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ export const dict: Record<string, string> = {
724724
"session.review.noChanges": "কোনো পরিবর্তন নেই",
725725
"session.review.noUncommittedChanges": "এখনও কোন অপ্রতিরোধ্য পরিবর্তন",
726726
"session.review.noBranchChanges": "এখনো কোনো শাখা পরিবর্তন হয়নি",
727+
"session.files.refresh": "রিফ্রেশ",
727728
"session.files.selectToOpen": "খুলতে একটি ফাইল নির্বাচন করুন",
728729
"session.files.all": "সমস্ত ফাইল",
729730
"session.files.empty": "কোনো ফাইল নেই",

packages/app/src/i18n/br.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,7 @@ export const dict = {
714714
"session.review.noChanges": "Sem alterações",
715715
"session.review.noUncommittedChanges": "Ainda não há alterações sem commit",
716716
"session.review.noBranchChanges": "Ainda não há alterações na branch",
717+
"session.files.refresh": "Atualizar",
717718
"session.files.selectToOpen": "Selecione um arquivo para abrir",
718719
"session.files.all": "Todos os arquivos",
719720
"session.files.empty": "Nenhum arquivo",

packages/app/src/i18n/bs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,7 @@ export const dict = {
768768
"session.review.noUncommittedChanges": "Još nema promjena koje nisu commitovane",
769769
"session.review.noBranchChanges": "Još nema promjena na grani",
770770

771+
"session.files.refresh": "Osvježi",
771772
"session.files.selectToOpen": "Odaberi datoteku za otvaranje",
772773
"session.files.all": "Sve datoteke",
773774
"session.files.empty": "Nema datoteka",

0 commit comments

Comments
 (0)