diff --git a/packages/app/src/context/command-keybind.test.ts b/packages/app/src/context/command-keybind.test.ts
index 4e38efd8da..d804195c40 100644
--- a/packages/app/src/context/command-keybind.test.ts
+++ b/packages/app/src/context/command-keybind.test.ts
@@ -40,4 +40,11 @@ describe("command keybind helpers", () => {
expect(display.includes("Alt") || display.includes("⌥")).toBe(true)
expect(formatKeybind("none")).toBe("")
})
+
+ test("formatKeybind prefers the first combo", () => {
+ const display = formatKeybind("mod+k,mod+p")
+
+ expect(display.includes("K") || display.includes("k")).toBe(true)
+ expect(display.includes("P") || display.includes("p")).toBe(false)
+ })
})
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 72caed40ad..579b740d3a 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -23,6 +23,8 @@ export const dict = {
"command.sidebar.toggle": "Toggle sidebar",
"command.project.open": "Open project",
+ "command.project.previous": "Previous project",
+ "command.project.next": "Next project",
"command.provider.connect": "Connect provider",
"command.server.switch": "Switch server",
"command.settings.open": "Open settings",
@@ -274,7 +276,7 @@ export const dict = {
"prompt.context.includeActiveFile": "Include active file",
"prompt.context.removeActiveFile": "Remove active file from context",
"prompt.context.removeFile": "Remove file from context",
- "prompt.action.attachFile": "Add file",
+ "prompt.action.attachFile": "Add files",
"prompt.attachment.remove": "Remove attachment",
"prompt.action.send": "Send",
"prompt.action.stop": "Stop",
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 8e2248469d..d8b0732580 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -211,13 +211,22 @@ export default function Layout(props: ParentProps) {
onMount(() => {
const stop = () => setState("sizing", false)
+ const blur = () => reset()
+ const hide = () => {
+ if (document.visibilityState !== "hidden") return
+ reset()
+ }
window.addEventListener("pointerup", stop)
window.addEventListener("pointercancel", stop)
window.addEventListener("blur", stop)
+ window.addEventListener("blur", blur)
+ document.addEventListener("visibilitychange", hide)
onCleanup(() => {
window.removeEventListener("pointerup", stop)
window.removeEventListener("pointercancel", stop)
window.removeEventListener("blur", stop)
+ window.removeEventListener("blur", blur)
+ document.removeEventListener("visibilitychange", hide)
})
})
@@ -237,6 +246,12 @@ export default function Layout(props: ParentProps) {
navLeave.current = undefined
}
+ const reset = () => {
+ disarm()
+ setState("hoverSession", undefined)
+ setHoverProject(undefined)
+ }
+
const arm = () => {
if (layout.sidebar.opened()) return
if (state.hoverProject === undefined) return
@@ -305,8 +320,7 @@ export default function Layout(props: ParentProps) {
const clearSidebarHoverState = () => {
if (layout.sidebar.opened()) return
- setState("hoverSession", undefined)
- setHoverProject(undefined)
+ reset()
}
const navigateWithSidebarReset = (href: string) => {
@@ -936,6 +950,26 @@ export default function Layout(props: ParentProps) {
navigateToSession(session)
}
+ function navigateProjectByOffset(offset: number) {
+ const projects = layout.projects.list()
+ if (projects.length === 0) return
+
+ const current = currentProject()?.worktree
+ const fallback = currentDir() ? projectRoot(currentDir()) : undefined
+ const active = current ?? fallback
+ const index = active ? projects.findIndex((project) => project.worktree === active) : -1
+
+ const target =
+ index === -1
+ ? offset > 0
+ ? projects[0]
+ : projects[projects.length - 1]
+ : projects[(index + offset + projects.length) % projects.length]
+ if (!target) return
+
+ openProject(target.worktree)
+ }
+
function navigateSessionByUnseen(offset: number) {
const sessions = currentSessions()
if (sessions.length === 0) return
@@ -1002,6 +1036,20 @@ export default function Layout(props: ParentProps) {
keybind: "mod+o",
onSelect: () => chooseProject(),
},
+ {
+ id: "project.previous",
+ title: language.t("command.project.previous"),
+ category: language.t("command.category.project"),
+ keybind: "mod+alt+arrowup",
+ onSelect: () => navigateProjectByOffset(-1),
+ },
+ {
+ id: "project.next",
+ title: language.t("command.project.next"),
+ category: language.t("command.category.project"),
+ keybind: "mod+alt+arrowdown",
+ onSelect: () => navigateProjectByOffset(1),
+ },
{
id: "provider.connect",
title: language.t("command.provider.connect"),
@@ -1941,6 +1989,10 @@ export default function Layout(props: ParentProps) {
onProjectMouseEnter: (worktree, event) => aim.enter(worktree, event),
onProjectMouseLeave: (worktree) => aim.leave(worktree),
onProjectFocus: (worktree) => aim.activate(worktree),
+ onHoverOpenChanged: (worktree, hoverOpen) => {
+ if (!hoverOpen && state.hoverProject && state.hoverProject !== worktree) return
+ setState("hoverProject", hoverOpen ? worktree : undefined)
+ },
navigateToProject,
openSidebar: () => layout.sidebar.open(),
closeProject,
@@ -2334,14 +2386,12 @@ export default function Layout(props: ParentProps) {
size={layout.sidebar.width()}
min={244}
max={typeof window === "undefined" ? 1000 : window.innerWidth * 0.3 + 64}
- collapseThreshold={244}
onResize={(w) => {
setState("sizing", true)
if (sizet !== undefined) clearTimeout(sizet)
sizet = window.setTimeout(() => setState("sizing", false), 120)
layout.sidebar.resize(w)
}}
- onCollapse={layout.sidebar.close}
/>