summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-03 05:19:14 -0600
committerAdam <[email protected]>2026-02-03 05:19:43 -0600
commite709808b3214afcf5ab8582829d1339a9eac79c0 (patch)
tree0bef9f8387cb0ee054d290fd23b3ce64a61e71f2 /packages/app/src/components
parent0d22068c90430e217c9c903a003b22668890207c (diff)
downloadopencode-e709808b3214afcf5ab8582829d1339a9eac79c0.tar.gz
opencode-e709808b3214afcf5ab8582829d1339a9eac79c0.zip
fix(app): move session search to command palette
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/dialog-select-file.tsx186
1 files changed, 168 insertions, 18 deletions
diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx
index 64b83d31b..167f21195 100644
--- a/packages/app/src/components/dialog-select-file.tsx
+++ b/packages/app/src/components/dialog-select-file.tsx
@@ -1,17 +1,22 @@
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
import { FileIcon } from "@opencode-ai/ui/file-icon"
+import { Icon } from "@opencode-ai/ui/icon"
import { Keybind } from "@opencode-ai/ui/keybind"
import { List } from "@opencode-ai/ui/list"
+import { base64Encode } from "@opencode-ai/util/encode"
import { getDirectory, getFilename } from "@opencode-ai/util/path"
-import { useParams } from "@solidjs/router"
-import { createMemo, createSignal, onCleanup, Show } from "solid-js"
+import { useNavigate, useParams } from "@solidjs/router"
+import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js"
import { formatKeybind, useCommand, type CommandOption } from "@/context/command"
+import { useGlobalSDK } from "@/context/global-sdk"
+import { useGlobalSync } from "@/context/global-sync"
import { useLayout } from "@/context/layout"
import { useFile } from "@/context/file"
import { useLanguage } from "@/context/language"
+import { decode64 } from "@/utils/base64"
-type EntryType = "command" | "file"
+type EntryType = "command" | "file" | "session"
type Entry = {
id: string
@@ -22,6 +27,9 @@ type Entry = {
category: string
option?: CommandOption
path?: string
+ directory?: string
+ sessionID?: string
+ archived?: number
}
type DialogSelectFileMode = "all" | "files"
@@ -33,6 +41,9 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const file = useFile()
const dialog = useDialog()
const params = useParams()
+ const navigate = useNavigate()
+ const globalSDK = useGlobalSDK()
+ const globalSync = useGlobalSync()
const filesOnly = () => props.mode === "files"
const sessionKey = createMemo(() => `${params.dir}${params.id ? "/" + params.id : ""}`)
const tabs = createMemo(() => layout.tabs(sessionKey))
@@ -73,6 +84,52 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
path,
})
+ const projectDirectory = createMemo(() => decode64(params.dir) ?? "")
+ const project = createMemo(() => {
+ const directory = projectDirectory()
+ if (!directory) return
+ return layout.projects.list().find((p) => p.worktree === directory || p.sandboxes?.includes(directory))
+ })
+ const workspaces = createMemo(() => {
+ const directory = projectDirectory()
+ const current = project()
+ if (!current) return directory ? [directory] : []
+
+ const dirs = [current.worktree, ...(current.sandboxes ?? [])]
+ if (directory && !dirs.includes(directory)) return [...dirs, directory]
+ return dirs
+ })
+ const homedir = createMemo(() => globalSync.data.path.home)
+ const label = (directory: string) => {
+ const current = project()
+ const kind =
+ current && directory === current.worktree
+ ? language.t("workspace.type.local")
+ : language.t("workspace.type.sandbox")
+ const [store] = globalSync.child(directory, { bootstrap: false })
+ const home = homedir()
+ const path = home ? directory.replace(home, "~") : directory
+ const name = store.vcs?.branch ?? getFilename(directory)
+ return `${kind} : ${name || path}`
+ }
+
+ const sessionItem = (input: {
+ directory: string
+ id: string
+ title: string
+ description: string
+ archived?: number
+ }): Entry => ({
+ id: `session:${input.directory}:${input.id}`,
+ type: "session",
+ title: input.title,
+ description: input.description,
+ category: language.t("command.category.session"),
+ directory: input.directory,
+ sessionID: input.id,
+ archived: input.archived,
+ })
+
const list = createMemo(() => allowed().map(commandItem))
const picks = createMemo(() => {
@@ -122,6 +179,68 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
return out
}
+ const sessionToken = { value: 0 }
+ let sessionInflight: Promise<Entry[]> | undefined
+ let sessionAll: Entry[] | undefined
+
+ const sessions = (text: string) => {
+ const query = text.trim()
+ if (!query) {
+ sessionToken.value += 1
+ sessionInflight = undefined
+ sessionAll = undefined
+ return [] as Entry[]
+ }
+
+ if (sessionAll) return sessionAll
+ if (sessionInflight) return sessionInflight
+
+ const current = sessionToken.value
+ const dirs = workspaces()
+ if (dirs.length === 0) return [] as Entry[]
+
+ sessionInflight = Promise.all(
+ dirs.map((directory) => {
+ const description = label(directory)
+ return globalSDK.client.session
+ .list({ directory, roots: true })
+ .then((x) =>
+ (x.data ?? [])
+ .filter((s) => !!s?.id)
+ .map((s) => ({
+ id: s.id,
+ title: s.title ?? language.t("command.session.new"),
+ description,
+ directory,
+ archived: s.time?.archived,
+ })),
+ )
+ .catch(() => [] as { id: string; title: string; description: string; directory: string; archived?: number }[])
+ }),
+ )
+ .then((results) => {
+ if (sessionToken.value !== current) return [] as Entry[]
+ const seen = new Set<string>()
+ const next = results
+ .flat()
+ .filter((item) => {
+ const key = `${item.directory}:${item.id}`
+ if (seen.has(key)) return false
+ seen.add(key)
+ return true
+ })
+ .map(sessionItem)
+ sessionAll = next
+ return next
+ })
+ .catch(() => [] as Entry[])
+ .finally(() => {
+ sessionInflight = undefined
+ })
+
+ return sessionInflight
+ }
+
const items = async (text: string) => {
const query = text.trim()
setGrouped(query.length > 0)
@@ -146,9 +265,10 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
const files = await file.searchFiles(query)
return files.map(fileItem)
}
- const files = await file.searchFiles(query)
+
+ const [files, nextSessions] = await Promise.all([file.searchFiles(query), Promise.resolve(sessions(query))])
const entries = files.map(fileItem)
- return [...list(), ...entries]
+ return [...list(), ...nextSessions, ...entries]
}
const handleMove = (item: Entry | undefined) => {
@@ -178,6 +298,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
return
}
+ if (item.type === "session") {
+ if (!item.directory || !item.sessionID) return
+ navigate(`/${base64Encode(item.directory)}/session/${item.sessionID}`)
+ return
+ }
+
if (!item.path) return
open(item.path)
}
@@ -202,13 +328,12 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
items={items}
key={(item) => item.id}
filterKeys={["title", "description", "category"]}
- groupBy={(item) => item.category}
+ groupBy={grouped() ? (item) => item.category : () => ""}
onMove={handleMove}
onSelect={handleSelect}
>
{(item) => (
- <Show
- when={item.type === "command"}
+ <Switch
fallback={
<div class="w-full flex items-center justify-between rounded-md pl-1">
<div class="flex items-center gap-x-3 grow min-w-0">
@@ -223,18 +348,43 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
</div>
}
>
- <div class="w-full flex items-center justify-between gap-4">
- <div class="flex items-center gap-2 min-w-0">
- <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
- <Show when={item.description}>
- <span class="text-14-regular text-text-weak truncate">{item.description}</span>
+ <Match when={item.type === "command"}>
+ <div class="w-full flex items-center justify-between gap-4">
+ <div class="flex items-center gap-2 min-w-0">
+ <span class="text-14-regular text-text-strong whitespace-nowrap">{item.title}</span>
+ <Show when={item.description}>
+ <span class="text-14-regular text-text-weak truncate">{item.description}</span>
+ </Show>
+ </div>
+ <Show when={item.keybind}>
+ <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
</Show>
</div>
- <Show when={item.keybind}>
- <Keybind class="rounded-[4px]">{formatKeybind(item.keybind ?? "")}</Keybind>
- </Show>
- </div>
- </Show>
+ </Match>
+ <Match when={item.type === "session"}>
+ <div class="w-full flex items-center justify-between rounded-md pl-1">
+ <div class="flex items-center gap-x-3 grow min-w-0">
+ <Icon name="bubble-5" size="small" class="shrink-0 text-icon-weak" />
+ <div class="flex items-center gap-2 min-w-0">
+ <span
+ class="text-14-regular text-text-strong truncate"
+ classList={{ "opacity-70": !!item.archived }}
+ >
+ {item.title}
+ </span>
+ <Show when={item.description}>
+ <span
+ class="text-14-regular text-text-weak truncate"
+ classList={{ "opacity-70": !!item.archived }}
+ >
+ {item.description}
+ </span>
+ </Show>
+ </div>
+ </div>
+ </div>
+ </Match>
+ </Switch>
)}
</List>
</Dialog>