Skip to content

Commit d0a5730

Browse files
authored
app: file type filter on desktop + multiple files (#18403)
1 parent 27a70ad commit d0a5730

9 files changed

Lines changed: 120 additions & 60 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,11 +1383,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
13831383
<input
13841384
ref={fileInputRef}
13851385
type="file"
1386+
multiple
13861387
accept={ACCEPTED_FILE_TYPES.join(",")}
13871388
class="hidden"
13881389
onChange={(e) => {
1389-
const file = e.currentTarget.files?.[0]
1390-
if (file) void addAttachment(file)
1390+
const list = e.currentTarget.files
1391+
if (list) {
1392+
for (const file of Array.from(list)) {
1393+
void addAttachment(file)
1394+
}
1395+
}
13911396
e.currentTarget.value = ""
13921397
}}
13931398
/>

packages/app/src/components/prompt-input/files.ts

Lines changed: 3 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
1+
import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
2+
3+
export { ACCEPTED_FILE_TYPES }
24

35
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
46
const IMAGE_EXTS = new Map([
@@ -18,61 +20,6 @@ const TEXT_MIMES = new Set([
1820
"application/yaml",
1921
])
2022

21-
export const ACCEPTED_FILE_TYPES = [
22-
...ACCEPTED_IMAGE_TYPES,
23-
"application/pdf",
24-
"text/*",
25-
"application/json",
26-
"application/ld+json",
27-
"application/toml",
28-
"application/x-toml",
29-
"application/x-yaml",
30-
"application/xml",
31-
"application/yaml",
32-
".c",
33-
".cc",
34-
".cjs",
35-
".conf",
36-
".cpp",
37-
".css",
38-
".csv",
39-
".cts",
40-
".env",
41-
".go",
42-
".gql",
43-
".graphql",
44-
".h",
45-
".hh",
46-
".hpp",
47-
".htm",
48-
".html",
49-
".ini",
50-
".java",
51-
".js",
52-
".json",
53-
".jsx",
54-
".log",
55-
".md",
56-
".mdx",
57-
".mjs",
58-
".mts",
59-
".py",
60-
".rb",
61-
".rs",
62-
".sass",
63-
".scss",
64-
".sh",
65-
".sql",
66-
".toml",
67-
".ts",
68-
".tsx",
69-
".txt",
70-
".xml",
71-
".yaml",
72-
".yml",
73-
".zsh",
74-
]
75-
7623
const SAMPLE = 4096
7724

7825
function kind(type: string) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
2+
3+
export const ACCEPTED_FILE_TYPES = [
4+
...ACCEPTED_IMAGE_TYPES,
5+
"application/pdf",
6+
"text/*",
7+
"application/json",
8+
"application/ld+json",
9+
"application/toml",
10+
"application/x-toml",
11+
"application/x-yaml",
12+
"application/xml",
13+
"application/yaml",
14+
".c",
15+
".cc",
16+
".cjs",
17+
".conf",
18+
".cpp",
19+
".css",
20+
".csv",
21+
".cts",
22+
".env",
23+
".go",
24+
".gql",
25+
".graphql",
26+
".h",
27+
".hh",
28+
".hpp",
29+
".htm",
30+
".html",
31+
".ini",
32+
".java",
33+
".js",
34+
".json",
35+
".jsx",
36+
".log",
37+
".md",
38+
".mdx",
39+
".mjs",
40+
".mts",
41+
".py",
42+
".rb",
43+
".rs",
44+
".sass",
45+
".scss",
46+
".sh",
47+
".sql",
48+
".toml",
49+
".ts",
50+
".tsx",
51+
".txt",
52+
".xml",
53+
".yaml",
54+
".yml",
55+
".zsh",
56+
]
57+
58+
const MIME_EXT = new Map([
59+
["image/png", "png"],
60+
["image/jpeg", "jpg"],
61+
["image/gif", "gif"],
62+
["image/webp", "webp"],
63+
["application/pdf", "pdf"],
64+
["application/json", "json"],
65+
["application/ld+json", "jsonld"],
66+
["application/toml", "toml"],
67+
["application/x-toml", "toml"],
68+
["application/x-yaml", "yaml"],
69+
["application/xml", "xml"],
70+
["application/yaml", "yaml"],
71+
])
72+
73+
const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"]
74+
75+
export const ACCEPTED_FILE_EXTENSIONS = Array.from(
76+
new Set(
77+
ACCEPTED_FILE_TYPES.flatMap((item) => {
78+
if (item.startsWith(".")) return [item.slice(1)]
79+
if (item === "text/*") return TEXT_EXT
80+
const out = MIME_EXT.get(item)
81+
return out ? [out] : []
82+
}),
83+
),
84+
).sort()
85+
86+
export function filePickerFilters(ext?: string[]) {
87+
if (!ext || ext.length === 0) return undefined
88+
return [{ name: "Files", extensions: ext }]
89+
}

packages/app/src/context/platform.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ServerConnection } from "./server"
55

66
type PickerPaths = string | string[] | null
77
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
8-
type OpenFilePickerOptions = { title?: string; multiple?: boolean }
8+
type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
99
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
1010
type UpdateInfo = { updateAvailable: boolean; version?: string }
1111

packages/app/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { AppBaseProviders, AppInterface } from "./app"
2+
export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
23
export { useCommand } from "./context/command"
34
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
45
export { ServerConnection } from "./context/server"

packages/desktop-electron/src/main/ipc.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme,
66
import { getStore } from "./store"
77
import { setTitlebar } from "./windows"
88

9+
const pickerFilters = (ext?: string[]) => {
10+
if (!ext || ext.length === 0) return undefined
11+
return [{ name: "Files", extensions: ext }]
12+
}
13+
914
type Deps = {
1015
killSidecar: () => void
1116
installCli: () => Promise<string>
@@ -94,11 +99,15 @@ export function registerIpcHandlers(deps: Deps) {
9499

95100
ipcMain.handle(
96101
"open-file-picker",
97-
async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
102+
async (
103+
_event: IpcMainInvokeEvent,
104+
opts?: { multiple?: boolean; title?: string; defaultPath?: string; accept?: string[]; extensions?: string[] },
105+
) => {
98106
const result = await dialog.showOpenDialog({
99107
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
100108
title: opts?.title ?? "Choose a file",
101109
defaultPath: opts?.defaultPath,
110+
filters: pickerFilters(opts?.extensions),
102111
})
103112
if (result.canceled) return null
104113
return opts?.multiple ? result.filePaths : result.filePaths[0]

packages/desktop-electron/src/preload/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ export type ElectronAPI = {
5050
multiple?: boolean
5151
title?: string
5252
defaultPath?: string
53+
accept?: string[]
54+
extensions?: string[]
5355
}) => Promise<string | string[] | null>
5456
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
5557
openLink: (url: string) => void

packages/desktop-electron/src/renderer/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// @refresh reload
22

33
import {
4+
ACCEPTED_FILE_EXTENSIONS,
5+
ACCEPTED_FILE_TYPES,
46
AppBaseProviders,
57
AppInterface,
68
handleNotificationClick,
@@ -111,6 +113,8 @@ const createPlatform = (): Platform => {
111113
const result = await window.api.openFilePicker({
112114
multiple: opts?.multiple ?? false,
113115
title: opts?.title ?? t("desktop.dialog.chooseFile"),
116+
accept: opts?.accept ?? ACCEPTED_FILE_TYPES,
117+
extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS,
114118
})
115119
return handleWslPicker(result)
116120
},

packages/desktop/src/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// @refresh reload
22

33
import {
4+
ACCEPTED_FILE_EXTENSIONS,
5+
filePickerFilters,
46
AppBaseProviders,
57
AppInterface,
68
handleNotificationClick,
@@ -98,6 +100,7 @@ const createPlatform = (): Platform => {
98100
directory: false,
99101
multiple: opts?.multiple ?? false,
100102
title: opts?.title ?? t("desktop.dialog.chooseFile"),
103+
filters: filePickerFilters(opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS),
101104
})
102105
return handleWslPicker(result)
103106
},

0 commit comments

Comments
 (0)