From 114e1647c03fe40f2cc0bd94f7402cb2c289ef3b Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Fri, 11 Sep 2026 05:25:20 +0000 Subject: [PATCH 1/2] feat: add hidden files toggle to remote file picker --- .../Views/FileSync/FilePicker.swift | 26 ++++++--- .../Coder-DesktopTests/FilePickerTests.swift | 57 +++++++++++++++++++ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift index 24e938a4..e7bdc2b2 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift @@ -6,6 +6,7 @@ struct FilePicker: View { @Environment(\.dismiss) var dismiss @StateObject private var model: FilePickerModel @State private var selection: FilePickerEntryModel? + @AppStorage("ShowHiddenFilesInRemoteFilePicker") private var showHiddenFiles = false @Binding var outputAbsPath: String @@ -34,8 +35,8 @@ struct FilePicker: View { .padding() } else { List(selection: $selection) { - ForEach(model.rootEntries) { entry in - FilePickerEntry(entry: entry).tag(entry) + ForEach(model.rootEntries.filter { showHiddenFiles || !$0.isHidden }) { entry in + FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry) } }.contextMenu( forSelectionType: FilePickerEntryModel.self, @@ -49,6 +50,9 @@ struct FilePicker: View { } Divider() HStack { + Toggle(isOn: $showHiddenFiles) { + Text("Show hidden files") + } Spacer() Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction) Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil) @@ -57,6 +61,11 @@ struct FilePicker: View { .onAppear { model.loadRoot() } + .onChange(of: showHiddenFiles) { + if !showHiddenFiles, selection?.isHidden == true { + selection = nil + } + } .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector } @@ -99,6 +108,7 @@ class FilePickerModel: ObservableObject { struct FilePickerEntry: View { @ObservedObject var entry: FilePickerEntryModel + let showHiddenFiles: Bool var body: some View { Group { @@ -116,8 +126,8 @@ struct FilePickerEntry: View { private var directory: some View { DisclosureGroup(isExpanded: $entry.isExpanded) { if let entries = entry.entries { - ForEach(entries) { entry in - FilePickerEntry(entry: entry).tag(entry) + ForEach(entries.filter { showHiddenFiles || !$0.isHidden }) { entry in + FilePickerEntry(entry: entry, showHiddenFiles: showHiddenFiles).tag(entry) } } } label: { @@ -150,6 +160,7 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject { let path: [String] let absolute_path: String let dir: Bool + var isHidden: Bool { path.contains { $0.hasPrefix(".") } } let client: AgentClient @@ -221,11 +232,8 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject { extension LSResponse { @MainActor func toModels(client: AgentClient) -> [FilePickerEntryModel] { - contents.compactMap { entry in - // Filter dotfiles from the picker - guard !entry.name.hasPrefix(".") else { return nil } - - return FilePickerEntryModel( + contents.map { entry in + FilePickerEntryModel( name: entry.name, client: client, absolute_path: entry.absolute_path_string, diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift index 7fde3334..44542d89 100644 --- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift @@ -77,6 +77,63 @@ struct FilePickerTests { } } + @Test + func testHiddenFilesToggle() async throws { + let key = "ShowHiddenFilesInRemoteFilePicker" + UserDefaults.standard.removeObject(forKey: key) + defer { UserDefaults.standard.removeObject(forKey: key) } + + let hiddenMockResponse = LSResponse( + absolute_path: ["/"], + absolute_path_string: "/", + contents: [ + LSFile(name: ".mux", absolute_path_string: "/.mux", is_dir: true), + LSFile(name: ".bashrc", absolute_path_string: "/.bashrc", is_dir: false), + LSFile(name: "home", absolute_path_string: "/home", is_dir: true), + LSFile(name: "tmp", absolute_path_string: "/tmp", is_dir: true), + LSFile(name: "etc", absolute_path_string: "/etc", is_dir: true), + LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false), + ] + ) + let host = "test-hidden-toggle.coder" + let sut = FilePicker(host: host, outputAbsPath: .constant("")) + let view = sut + + let url = URL(string: "http://\(host):4")! + + try Mock( + url: url.appendingPathComponent("/api/v0/list-directory"), + statusCode: 200, + data: [.post: CoderSDK.encoder.encode(hiddenMockResponse)] + ).register() + + try await ViewHosting.host(view) { + try await sut.inspection.inspect { view in + try #expect(await eventually { @MainActor in + _ = try view.find(ViewType.List.self) + return true + }) + _ = try view.find(text: "README.md") + var toggle = try view.find(ViewType.Toggle.self) + #expect(try toggle.labelView().text().string() == "Show hidden files") + #expect(try !toggle.isOn()) + #expect(throws: (any Error).self) { _ = try view.find(text: ".mux") } + + try toggle.tap() + toggle = try view.find(ViewType.Toggle.self) + #expect(try toggle.isOn()) + #expect(UserDefaults.standard.bool(forKey: key)) + _ = try view.find(text: ".mux") + + try toggle.tap() + toggle = try view.find(ViewType.Toggle.self) + #expect(try !toggle.isOn()) + #expect(!UserDefaults.standard.bool(forKey: key)) + #expect(throws: (any Error).self) { _ = try view.find(text: ".mux") } + } + } + } + @Test func testDirectoryExpansion() async throws { let host = "test-expansion.coder" From dcafabfc82bb926a86195e4afdbfcae90ec683f7 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 15 Sep 2026 02:28:37 +0000 Subject: [PATCH 2/2] test: trim hidden files toggle fixture and assert directory rows --- Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift index 44542d89..b34fcd5f 100644 --- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift @@ -88,10 +88,7 @@ struct FilePickerTests { absolute_path_string: "/", contents: [ LSFile(name: ".mux", absolute_path_string: "/.mux", is_dir: true), - LSFile(name: ".bashrc", absolute_path_string: "/.bashrc", is_dir: false), LSFile(name: "home", absolute_path_string: "/home", is_dir: true), - LSFile(name: "tmp", absolute_path_string: "/tmp", is_dir: true), - LSFile(name: "etc", absolute_path_string: "/etc", is_dir: true), LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false), ] ) @@ -118,18 +115,21 @@ struct FilePickerTests { #expect(try toggle.labelView().text().string() == "Show hidden files") #expect(try !toggle.isOn()) #expect(throws: (any Error).self) { _ = try view.find(text: ".mux") } + #expect(view.findAll(ViewType.DisclosureGroup.self).count == 1) try toggle.tap() toggle = try view.find(ViewType.Toggle.self) #expect(try toggle.isOn()) #expect(UserDefaults.standard.bool(forKey: key)) _ = try view.find(text: ".mux") + #expect(view.findAll(ViewType.DisclosureGroup.self).count == 2) try toggle.tap() toggle = try view.find(ViewType.Toggle.self) #expect(try !toggle.isOn()) #expect(!UserDefaults.standard.bool(forKey: key)) #expect(throws: (any Error).self) { _ = try view.find(text: ".mux") } + #expect(view.findAll(ViewType.DisclosureGroup.self).count == 1) } } }