feat(app): search sessions by ID (#44387)
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
62e3b6ad78
commit
de11122c96
@@ -30,6 +30,7 @@ export function HomeCommandPalette(props: {
|
||||
opened: server.projects.list,
|
||||
stored: () => server.sync.data.project,
|
||||
load: (search, signal) => server.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
get: (sessionID, signal) => server.sdk.api.session.get({ sessionID }, { signal }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
|
||||
@@ -137,6 +137,17 @@ export function createHomeSessionsController(home: HomeController) {
|
||||
showProjectName: () => !home.project.selected(),
|
||||
server: () => home.selection.value().server,
|
||||
canCreate: () => !!home.project.newSession(),
|
||||
lookup: async (sessionID: string) => {
|
||||
const ctx = home.server.focusedContext()
|
||||
if (!ctx) return
|
||||
const result = await ctx.sdk.api.session.get({ sessionID })
|
||||
if (result.time.archived) return
|
||||
return buildHomeSessionRecords({
|
||||
sessions: () => [result],
|
||||
projectDirectories,
|
||||
projects: home.project.list,
|
||||
})[0]
|
||||
},
|
||||
create: home.project.openNewSession,
|
||||
open: (session: SessionInfo, options?: OpenSessionOptions) => {
|
||||
const directoryKey = pathKey(session.location.directory)
|
||||
|
||||
@@ -8,13 +8,21 @@ import { createMemo, onCleanup } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import type { HomeController } from "../model"
|
||||
import { homeSessionSearchKey, type HomeSessionRecord, type HomeSessionsController } from "./controller"
|
||||
import { looksLikeSessionID } from "@/session/search"
|
||||
|
||||
type HomeSessionSearchSource = Pick<HomeSessionsController, "data" | "session">
|
||||
|
||||
export function createHomeSessionSearchController(home: HomeController, sessions: HomeSessionSearchSource) {
|
||||
const command = useCommand()
|
||||
const language = useLanguage()
|
||||
const [state, setState] = createStore({ value: "", focused: false, highlighted: "" })
|
||||
const [state, setState] = createStore({
|
||||
value: "",
|
||||
focused: false,
|
||||
highlighted: "",
|
||||
exact: undefined as HomeSessionRecord | undefined,
|
||||
lookingUp: false,
|
||||
})
|
||||
let lookup = 0
|
||||
let root: HTMLDivElement | undefined
|
||||
let input: HTMLInputElement | undefined
|
||||
let list: HTMLDivElement | undefined
|
||||
@@ -22,9 +30,11 @@ export function createHomeSessionSearchController(home: HomeController, sessions
|
||||
const results = createMemo(() => {
|
||||
const value = query().toLowerCase()
|
||||
if (!value) return []
|
||||
return sessions.data
|
||||
const records = sessions.data
|
||||
.searchRecords()
|
||||
.filter((record) => `${sessionLabel(record.session)} ${record.projectName}`.toLowerCase().includes(value))
|
||||
if (!state.exact || records.some((record) => record.session.id === state.exact?.session.id)) return records
|
||||
return [state.exact, ...records]
|
||||
})
|
||||
const active = createMemo(() => {
|
||||
const records = results()
|
||||
@@ -50,6 +60,7 @@ export function createHomeSessionSearchController(home: HomeController, sessions
|
||||
close()
|
||||
}),
|
||||
)
|
||||
onCleanup(() => lookup++)
|
||||
|
||||
command.register("home.search", () => [
|
||||
{
|
||||
@@ -67,7 +78,26 @@ export function createHomeSessionSearchController(home: HomeController, sessions
|
||||
}
|
||||
|
||||
function close() {
|
||||
setState({ value: "", focused: false })
|
||||
lookup++
|
||||
setState({ value: "", focused: false, exact: undefined, lookingUp: false })
|
||||
}
|
||||
|
||||
function update(value: string) {
|
||||
const current = ++lookup
|
||||
const sessionID = value.trim()
|
||||
setState({ value, highlighted: "", exact: undefined, lookingUp: false })
|
||||
if (!looksLikeSessionID(sessionID)) return
|
||||
setState("lookingUp", true)
|
||||
void sessions.session.lookup(sessionID).then(
|
||||
(record) => {
|
||||
if (current !== lookup) return
|
||||
setState({ exact: record, lookingUp: false })
|
||||
},
|
||||
() => {
|
||||
if (current !== lookup) return
|
||||
setState("lookingUp", false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function select(record: HomeSessionRecord, options?: { background?: boolean }) {
|
||||
@@ -81,11 +111,11 @@ export function createHomeSessionSearchController(home: HomeController, sessions
|
||||
placeholder,
|
||||
open,
|
||||
focus,
|
||||
input: (value: string) => setState({ value, highlighted: "" }),
|
||||
input: update,
|
||||
close,
|
||||
},
|
||||
result: {
|
||||
loading: sessions.data.loading,
|
||||
loading: () => sessions.data.loading() || state.lookingUp,
|
||||
list: results,
|
||||
active,
|
||||
noResultsLabel: () => language.t("home.sessions.search.noResults", { query: query() }),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function looksLikeSessionID(value: string) {
|
||||
return value.length > 20 && value.startsWith("ses_")
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { displayName, projectForSession } from "@/shell/layout/helpers"
|
||||
import { createSessionTabs } from "@/session/helpers"
|
||||
import { useSessionLayout } from "@/session/session-layout"
|
||||
import { useServer } from "@/runtime/server/current"
|
||||
import { looksLikeSessionID } from "@/session/search"
|
||||
|
||||
export type CommandPaletteEntry = {
|
||||
id: string
|
||||
@@ -147,6 +148,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
|
||||
opened: serverCtx.projects.list,
|
||||
stored: () => serverCtx.sync.data.project,
|
||||
load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
|
||||
get: (sessionID, signal) => serverSDK.api.session.get({ sessionID }, { signal }),
|
||||
untitled: () => language.t("command.session.new"),
|
||||
category: () => language.t("command.category.session"),
|
||||
})
|
||||
@@ -221,6 +223,7 @@ export function createServerSessionEntries(props: {
|
||||
opened: () => LocalProject[]
|
||||
stored: () => Project[]
|
||||
load: (search: string, signal: AbortSignal) => Promise<{ data: SessionInfo[] }>
|
||||
get: (sessionID: string, signal: AbortSignal) => Promise<SessionInfo>
|
||||
untitled: () => string
|
||||
category: () => string
|
||||
}) {
|
||||
@@ -253,28 +256,36 @@ export function createServerSessionEntries(props: {
|
||||
const openedByID = new Map(opened.flatMap((project) => (project.id ? [[project.id, project] as const] : [])))
|
||||
const stored = props.stored().map((project) => ({ ...project, expanded: false }))
|
||||
const storedByID = new Map(stored.map((project) => [project.id, project] as const))
|
||||
return props
|
||||
.load(search, current.signal)
|
||||
.then((result) =>
|
||||
result.data
|
||||
.filter((session) => !session.time.archived)
|
||||
.map((session) => {
|
||||
const project =
|
||||
projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID)
|
||||
return {
|
||||
id: `session:${props.server}:${session.id}`,
|
||||
type: "session" as const,
|
||||
title: session.title || props.untitled(),
|
||||
description: project ? displayName(project) : getFilename(session.location.directory),
|
||||
category: props.category(),
|
||||
directory: session.location.directory,
|
||||
sessionID: session.id,
|
||||
server: props.server,
|
||||
project,
|
||||
updated: session.time.updated,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch(() => [] as CommandPaletteEntry[])
|
||||
return Promise.all([
|
||||
props.load(search, current.signal).then(
|
||||
(result) => result.data,
|
||||
() => [],
|
||||
),
|
||||
looksLikeSessionID(search)
|
||||
? props.get(search, current.signal).then(
|
||||
(result) => [result],
|
||||
() => [],
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
]).then(([listed, exact]) =>
|
||||
[...new Map([...exact, ...listed].map((session) => [session.id, session] as const)).values()]
|
||||
.filter((session) => !session.time.archived)
|
||||
.map((session) => {
|
||||
const project =
|
||||
projectForSession(session, opened, openedByID) ?? projectForSession(session, stored, storedByID)
|
||||
return {
|
||||
id: `session:${props.server}:${session.id}`,
|
||||
type: "session" as const,
|
||||
title: session.title || props.untitled(),
|
||||
description: project ? displayName(project) : getFilename(session.location.directory),
|
||||
category: props.category(),
|
||||
directory: session.location.directory,
|
||||
sessionID: session.id,
|
||||
server: props.server,
|
||||
project,
|
||||
updated: session.time.updated,
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ describe("command palette sessions", () => {
|
||||
expanded: true,
|
||||
}
|
||||
const searches: string[] = []
|
||||
const gets: string[] = []
|
||||
const result = await new Promise<Awaited<ReturnType<ReturnType<typeof createServerSessionEntries>>>>(
|
||||
(resolve, reject) => {
|
||||
createRoot((dispose) => {
|
||||
@@ -49,6 +50,10 @@ describe("command palette sessions", () => {
|
||||
data: [session, { ...session, id: "archived-session", time: { ...session.time, archived: 3 } }],
|
||||
}
|
||||
},
|
||||
get: async (sessionID) => {
|
||||
gets.push(sessionID)
|
||||
return session
|
||||
},
|
||||
untitled: () => "Untitled",
|
||||
category: () => "Sessions",
|
||||
})
|
||||
@@ -66,6 +71,7 @@ describe("command palette sessions", () => {
|
||||
)
|
||||
|
||||
expect(searches).toEqual(["palette session"])
|
||||
expect(gets).toEqual([])
|
||||
expect(result).toHaveLength(1)
|
||||
expect(getProjectAvatarSource(result[0]?.project?.id, result[0]?.project?.icon)).toBe("home-project-avatar")
|
||||
expect(result[0]).toMatchObject({
|
||||
@@ -75,4 +81,31 @@ describe("command palette sessions", () => {
|
||||
project: { id: stored.id, icon: opened.icon },
|
||||
})
|
||||
})
|
||||
|
||||
test("loads an exact session when the query looks like an ID", async () => {
|
||||
const exact = { ...session, id: "ses_12345678901234567890123456", title: "Exact session" }
|
||||
const gets: string[] = []
|
||||
const result = await new Promise<Awaited<ReturnType<ReturnType<typeof createServerSessionEntries>>>>(
|
||||
(resolve, reject) => {
|
||||
createRoot((dispose) => {
|
||||
const search = createServerSessionEntries({
|
||||
server: ServerConnection.Key.make("selected-server"),
|
||||
opened: () => [],
|
||||
stored: () => [stored],
|
||||
load: async () => ({ data: [] }),
|
||||
get: async (sessionID) => {
|
||||
gets.push(sessionID)
|
||||
return exact
|
||||
},
|
||||
untitled: () => "Untitled",
|
||||
category: () => "Sessions",
|
||||
})
|
||||
search(exact.id).then(resolve, reject).finally(dispose)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
expect(gets).toEqual([exact.id])
|
||||
expect(result).toMatchObject([{ sessionID: exact.id, title: exact.title }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user