summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrendan Allan <[email protected]>2026-03-20 23:02:07 +0800
committerGitHub <[email protected]>2026-03-20 15:02:07 +0000
commitd0a57305efcf03f4fd69ca180d97ea85e6cb2f1d (patch)
treec4112592fa632f24544deab42e465b0e2c1d0ff6
parent27a70ad70f30faf30d159f56b394c01f9474c7a4 (diff)
downloadopencode-d0a57305efcf03f4fd69ca180d97ea85e6cb2f1d.tar.gz
opencode-d0a57305efcf03f4fd69ca180d97ea85e6cb2f1d.zip
app: file type filter on desktop + multiple files (#18403)
-rw-r--r--packages/app/src/components/prompt-input.tsx9
-rw-r--r--packages/app/src/components/prompt-input/files.ts59
-rw-r--r--packages/app/src/constants/file-picker.ts89
-rw-r--r--packages/app/src/context/platform.tsx2
-rw-r--r--packages/app/src/index.ts1
-rw-r--r--packages/desktop-electron/src/main/ipc.ts11
-rw-r--r--packages/desktop-electron/src/preload/types.ts2
-rw-r--r--packages/desktop-electron/src/renderer/index.tsx4
-rw-r--r--packages/desktop/src/index.tsx3
9 files changed, 120 insertions, 60 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 55cfaa490..f3d3e135d 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -1383,11 +1383,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<input
ref={fileInputRef}
type="file"
+ multiple
accept={ACCEPTED_FILE_TYPES.join(",")}
class="hidden"
onChange={(e) => {
- const file = e.currentTarget.files?.[0]
- if (file) void addAttachment(file)
+ const list = e.currentTarget.files
+ if (list) {
+ for (const file of Array.from(list)) {
+ void addAttachment(file)
+ }
+ }
e.currentTarget.value = ""
}}
/>
diff --git a/packages/app/src/components/prompt-input/files.ts b/packages/app/src/components/prompt-input/files.ts
index 594991d07..eae8af03d 100644
--- a/packages/app/src/components/prompt-input/files.ts
+++ b/packages/app/src/components/prompt-input/files.ts
@@ -1,4 +1,6 @@
-export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
+import { ACCEPTED_FILE_TYPES, ACCEPTED_IMAGE_TYPES } from "@/constants/file-picker"
+
+export { ACCEPTED_FILE_TYPES }
const IMAGE_MIMES = new Set(ACCEPTED_IMAGE_TYPES)
const IMAGE_EXTS = new Map([
@@ -18,61 +20,6 @@ const TEXT_MIMES = new Set([
"application/yaml",
])
-export const ACCEPTED_FILE_TYPES = [
- ...ACCEPTED_IMAGE_TYPES,
- "application/pdf",
- "text/*",
- "application/json",
- "application/ld+json",
- "application/toml",
- "application/x-toml",
- "application/x-yaml",
- "application/xml",
- "application/yaml",
- ".c",
- ".cc",
- ".cjs",
- ".conf",
- ".cpp",
- ".css",
- ".csv",
- ".cts",
- ".env",
- ".go",
- ".gql",
- ".graphql",
- ".h",
- ".hh",
- ".hpp",
- ".htm",
- ".html",
- ".ini",
- ".java",
- ".js",
- ".json",
- ".jsx",
- ".log",
- ".md",
- ".mdx",
- ".mjs",
- ".mts",
- ".py",
- ".rb",
- ".rs",
- ".sass",
- ".scss",
- ".sh",
- ".sql",
- ".toml",
- ".ts",
- ".tsx",
- ".txt",
- ".xml",
- ".yaml",
- ".yml",
- ".zsh",
-]
-
const SAMPLE = 4096
function kind(type: string) {
diff --git a/packages/app/src/constants/file-picker.ts b/packages/app/src/constants/file-picker.ts
new file mode 100644
index 000000000..c661bc8f3
--- /dev/null
+++ b/packages/app/src/constants/file-picker.ts
@@ -0,0 +1,89 @@
+export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
+
+export const ACCEPTED_FILE_TYPES = [
+ ...ACCEPTED_IMAGE_TYPES,
+ "application/pdf",
+ "text/*",
+ "application/json",
+ "application/ld+json",
+ "application/toml",
+ "application/x-toml",
+ "application/x-yaml",
+ "application/xml",
+ "application/yaml",
+ ".c",
+ ".cc",
+ ".cjs",
+ ".conf",
+ ".cpp",
+ ".css",
+ ".csv",
+ ".cts",
+ ".env",
+ ".go",
+ ".gql",
+ ".graphql",
+ ".h",
+ ".hh",
+ ".hpp",
+ ".htm",
+ ".html",
+ ".ini",
+ ".java",
+ ".js",
+ ".json",
+ ".jsx",
+ ".log",
+ ".md",
+ ".mdx",
+ ".mjs",
+ ".mts",
+ ".py",
+ ".rb",
+ ".rs",
+ ".sass",
+ ".scss",
+ ".sh",
+ ".sql",
+ ".toml",
+ ".ts",
+ ".tsx",
+ ".txt",
+ ".xml",
+ ".yaml",
+ ".yml",
+ ".zsh",
+]
+
+const MIME_EXT = new Map([
+ ["image/png", "png"],
+ ["image/jpeg", "jpg"],
+ ["image/gif", "gif"],
+ ["image/webp", "webp"],
+ ["application/pdf", "pdf"],
+ ["application/json", "json"],
+ ["application/ld+json", "jsonld"],
+ ["application/toml", "toml"],
+ ["application/x-toml", "toml"],
+ ["application/x-yaml", "yaml"],
+ ["application/xml", "xml"],
+ ["application/yaml", "yaml"],
+])
+
+const TEXT_EXT = ["txt", "text", "md", "markdown", "log", "csv"]
+
+export const ACCEPTED_FILE_EXTENSIONS = Array.from(
+ new Set(
+ ACCEPTED_FILE_TYPES.flatMap((item) => {
+ if (item.startsWith(".")) return [item.slice(1)]
+ if (item === "text/*") return TEXT_EXT
+ const out = MIME_EXT.get(item)
+ return out ? [out] : []
+ }),
+ ),
+).sort()
+
+export function filePickerFilters(ext?: string[]) {
+ if (!ext || ext.length === 0) return undefined
+ return [{ name: "Files", extensions: ext }]
+}
diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
index b8ed58e34..3bdc46391 100644
--- a/packages/app/src/context/platform.tsx
+++ b/packages/app/src/context/platform.tsx
@@ -5,7 +5,7 @@ import { ServerConnection } from "./server"
type PickerPaths = string | string[] | null
type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean }
-type OpenFilePickerOptions = { title?: string; multiple?: boolean }
+type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: string[]; extensions?: string[] }
type SaveFilePickerOptions = { title?: string; defaultPath?: string }
type UpdateInfo = { updateAvailable: boolean; version?: string }
diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
index 6c870dfa4..53063f48f 100644
--- a/packages/app/src/index.ts
+++ b/packages/app/src/index.ts
@@ -1,4 +1,5 @@
export { AppBaseProviders, AppInterface } from "./app"
+export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
export { useCommand } from "./context/command"
export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
export { ServerConnection } from "./context/server"
diff --git a/packages/desktop-electron/src/main/ipc.ts b/packages/desktop-electron/src/main/ipc.ts
index 71b3c3395..543f857a5 100644
--- a/packages/desktop-electron/src/main/ipc.ts
+++ b/packages/desktop-electron/src/main/ipc.ts
@@ -6,6 +6,11 @@ import type { InitStep, ServerReadyData, SqliteMigrationProgress, TitlebarTheme,
import { getStore } from "./store"
import { setTitlebar } from "./windows"
+const pickerFilters = (ext?: string[]) => {
+ if (!ext || ext.length === 0) return undefined
+ return [{ name: "Files", extensions: ext }]
+}
+
type Deps = {
killSidecar: () => void
installCli: () => Promise<string>
@@ -94,11 +99,15 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle(
"open-file-picker",
- async (_event: IpcMainInvokeEvent, opts?: { multiple?: boolean; title?: string; defaultPath?: string }) => {
+ async (
+ _event: IpcMainInvokeEvent,
+ opts?: { multiple?: boolean; title?: string; defaultPath?: string; accept?: string[]; extensions?: string[] },
+ ) => {
const result = await dialog.showOpenDialog({
properties: ["openFile", ...(opts?.multiple ? ["multiSelections" as const] : [])],
title: opts?.title ?? "Choose a file",
defaultPath: opts?.defaultPath,
+ filters: pickerFilters(opts?.extensions),
})
if (result.canceled) return null
return opts?.multiple ? result.filePaths : result.filePaths[0]
diff --git a/packages/desktop-electron/src/preload/types.ts b/packages/desktop-electron/src/preload/types.ts
index 100508fcd..f8e6d52c7 100644
--- a/packages/desktop-electron/src/preload/types.ts
+++ b/packages/desktop-electron/src/preload/types.ts
@@ -50,6 +50,8 @@ export type ElectronAPI = {
multiple?: boolean
title?: string
defaultPath?: string
+ accept?: string[]
+ extensions?: string[]
}) => Promise<string | string[] | null>
saveFilePicker: (opts?: { title?: string; defaultPath?: string }) => Promise<string | null>
openLink: (url: string) => void
diff --git a/packages/desktop-electron/src/renderer/index.tsx b/packages/desktop-electron/src/renderer/index.tsx
index 30e882e23..ec2b4d1e7 100644
--- a/packages/desktop-electron/src/renderer/index.tsx
+++ b/packages/desktop-electron/src/renderer/index.tsx
@@ -1,6 +1,8 @@
// @refresh reload
import {
+ ACCEPTED_FILE_EXTENSIONS,
+ ACCEPTED_FILE_TYPES,
AppBaseProviders,
AppInterface,
handleNotificationClick,
@@ -111,6 +113,8 @@ const createPlatform = (): Platform => {
const result = await window.api.openFilePicker({
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFile"),
+ accept: opts?.accept ?? ACCEPTED_FILE_TYPES,
+ extensions: opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS,
})
return handleWslPicker(result)
},
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx
index 65149f34b..e67795644 100644
--- a/packages/desktop/src/index.tsx
+++ b/packages/desktop/src/index.tsx
@@ -1,6 +1,8 @@
// @refresh reload
import {
+ ACCEPTED_FILE_EXTENSIONS,
+ filePickerFilters,
AppBaseProviders,
AppInterface,
handleNotificationClick,
@@ -98,6 +100,7 @@ const createPlatform = (): Platform => {
directory: false,
multiple: opts?.multiple ?? false,
title: opts?.title ?? t("desktop.dialog.chooseFile"),
+ filters: filePickerFilters(opts?.extensions ?? ACCEPTED_FILE_EXTENSIONS),
})
return handleWslPicker(result)
},