From 5fbcb203f5a2cab13c2f7468430b25be4989063b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 15 Dec 2025 06:34:08 -0600 Subject: [PATCH] wip(desktop): progress --- packages/desktop/src/context/command.tsx | 1 + packages/desktop/src/pages/layout.tsx | 150 ++++++++++++++++++++--- 2 files changed, 134 insertions(+), 17 deletions(-) diff --git a/packages/desktop/src/context/command.tsx b/packages/desktop/src/context/command.tsx index 26b03f980..dcc4b6007 100644 --- a/packages/desktop/src/context/command.tsx +++ b/packages/desktop/src/context/command.tsx @@ -139,6 +139,7 @@ function DialogCommand(props: { options: CommandOption[] }) { emptyMessage="No commands found" items={() => props.options.filter((x) => !x.id.startsWith("suggested.") || !x.disabled)} key={(x) => x?.id} + filterKeys={["title", "description", "category"]} groupBy={(x) => x.category ?? ""} onSelect={(option) => { if (option) { diff --git a/packages/desktop/src/pages/layout.tsx b/packages/desktop/src/pages/layout.tsx index df162c187..bb2302503 100644 --- a/packages/desktop/src/pages/layout.tsx +++ b/packages/desktop/src/pages/layout.tsx @@ -36,6 +36,7 @@ import { Binary } from "@opencode-ai/util/binary" import { Header } from "@/components/header" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectProvider } from "@/components/dialog-select-provider" +import { useCommand } from "@/context/command" export default function Layout(props: ParentProps) { const [store, setStore] = createStore({ @@ -52,6 +53,137 @@ export default function Layout(props: ParentProps) { const navigate = useNavigate() const providers = useProviders() const dialog = useDialog() + const command = useCommand() + + const currentSessions = createMemo(() => { + if (!params.dir) return [] + const directory = base64Decode(params.dir) + return globalSync.child(directory)[0].session ?? [] + }) + + function navigateSessionByOffset(offset: number) { + const projects = layout.projects.list() + if (projects.length === 0) return + + const currentDirectory = params.dir ? base64Decode(params.dir) : undefined + const projectIndex = currentDirectory ? projects.findIndex((p) => p.worktree === currentDirectory) : -1 + + // If we're not in any project, navigate to the first/last project based on direction + if (projectIndex === -1) { + const targetProject = offset > 0 ? projects[0] : projects[projects.length - 1] + if (targetProject) navigateToProject(targetProject.worktree) + return + } + + const sessions = currentSessions() + const sessionIndex = params.id ? sessions.findIndex((s) => s.id === params.id) : -1 + + // Calculate target index within current project + let targetIndex: number + if (sessionIndex === -1) { + // Not on a session - go to first session for "next", last session for "prev" + targetIndex = offset > 0 ? 0 : sessions.length - 1 + } else { + targetIndex = sessionIndex + offset + } + + // If target is within bounds, navigate to that session + if (targetIndex >= 0 && targetIndex < sessions.length) { + navigateToSession(sessions[targetIndex]) + return + } + + // Navigate to adjacent project + const nextProjectIndex = projectIndex + (offset > 0 ? 1 : -1) + const nextProject = projects[nextProjectIndex] + if (!nextProject) return + + const nextProjectSessions = globalSync.child(nextProject.worktree)[0].session ?? [] + if (nextProjectSessions.length === 0) { + // Navigate to the project's new session page if no sessions + navigateToProject(nextProject.worktree) + return + } + + // If going down (offset > 0), go to first session; if going up (offset < 0), go to last session + const targetSession = offset > 0 ? nextProjectSessions[0] : nextProjectSessions[nextProjectSessions.length - 1] + navigate(`/${base64Encode(nextProject.worktree)}/session/${targetSession.id}`) + } + + async function archiveSession(session: Session) { + const [store, setStore] = globalSync.child(session.directory) + const sessions = store.session ?? [] + const index = sessions.findIndex((s) => s.id === session.id) + // Get next session (prefer next, then prev) before removing + const nextSession = sessions[index + 1] ?? sessions[index - 1] + + await globalSDK.client.session.update({ + directory: session.directory, + sessionID: session.id, + time: { archived: Date.now() }, + }) + setStore( + produce((draft) => { + const match = Binary.search(draft.session, session.id, (s) => s.id) + if (match.found) draft.session.splice(match.index, 1) + }), + ) + if (session.id === params.id) { + if (nextSession) { + navigate(`/${params.dir}/session/${nextSession.id}`) + } else { + navigate(`/${params.dir}/session`) + } + } + } + + command.register(() => [ + { + id: "sidebar.toggle", + title: "Toggle sidebar", + category: "View", + keybind: "mod+b", + onSelect: () => layout.sidebar.toggle(), + }, + ...(platform.openDirectoryPickerDialog + ? [ + { + id: "project.open", + title: "Open project", + category: "Project", + keybind: "mod+o", + onSelect: () => chooseProject(), + }, + ] + : []), + { + id: "session.previous", + title: "Previous session", + category: "Session", + keybind: "alt+arrowup", + disabled: !params.dir, + onSelect: () => navigateSessionByOffset(-1), + }, + { + id: "session.next", + title: "Next session", + category: "Session", + keybind: "alt+arrowdown", + disabled: !params.dir, + onSelect: () => navigateSessionByOffset(1), + }, + { + id: "session.archive", + title: "Archive session", + category: "Session", + keybind: "mod+shift+backspace", + disabled: !params.dir || !params.id, + onSelect: () => { + const session = currentSessions().find((s) => s.id === params.id) + if (session) archiveSession(session) + }, + }, + ]) function connectProvider() { dialog.replace(() => ) @@ -293,22 +425,6 @@ export default function Layout(props: ParentProps) { session.id !== params.id && globalSync.child(props.project.worktree)[0].session_status[session.id]?.type === "busy", ) - async function archive(session: Session) { - await globalSDK.client.session.update({ - directory: session.directory, - sessionID: session.id, - time: { archived: Date.now() }, - }) - setStore( - produce((draft) => { - const match = Binary.search(draft.session, session.id, (s) => s.id) - if (match.found) draft.session.splice(match.index, 1) - }), - ) - if (session.id === params.id) { - navigate(`/${params.dir}/session`) - } - } return (