mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): open a folder as a project with ⌘O
workspace.openFolder (default mod+o, the editor-standard open-folder chord) runs openFolderAsProject: pick a folder, enter the project that already owns it or create one named after the folder, scope the sidebar, and land on a fresh session draft anchored there. A stale backend without the projects.* RPC still gets the workspace session, with a warning. StartWorkSessionRequest grows an openTab flag so these opens-from-nowhere stack a tab instead of spending an occupied main, and goToProject/ resolveNewSessionCwd share one projectRootCwd resolver.
This commit is contained in:
parent
8eb06e75b9
commit
5affcd6bf4
5 changed files with 92 additions and 6 deletions
|
|
@ -528,7 +528,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
lastStartWorkTokenRef.current = startWorkSessionRequest.token
|
||||
startSessionInWorkspace(startWorkSessionRequest.path)
|
||||
startSessionInWorkspace(startWorkSessionRequest.path, { openTab: startWorkSessionRequest.openTab })
|
||||
|
||||
if (startWorkSessionRequest.draft) {
|
||||
requestComposerInsert(startWorkSessionRequest.draft, { target: 'main' })
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import {
|
|||
switchToDefaultProfile,
|
||||
toggleShowAllProfiles
|
||||
} from '@/store/profile'
|
||||
import { requestNewWorktree } from '@/store/projects'
|
||||
import { openFolderAsProject, requestNewWorktree } from '@/store/projects'
|
||||
import { toggleReview } from '@/store/review'
|
||||
import { setModelPickerOpen } from '@/store/session'
|
||||
import { reopenLastClosedTile } from '@/store/session-states'
|
||||
|
|
@ -173,6 +173,9 @@ export function useKeybinds(deps: KeybindRuntimeDeps): void {
|
|||
// Only meaningful inside a git repo — a no-op otherwise (the key falls
|
||||
// through instead of silently doing nothing).
|
||||
'workspace.newWorktree': () => $repoStatus.get() && requestNewWorktree(),
|
||||
// ⌘O: native folder picker → open the folder as a project (upsert) with a
|
||||
// fresh session anchored there.
|
||||
'workspace.openFolder': () => void openFolderAsProject(),
|
||||
|
||||
// Narrow-viewport reveal is handled inside the store toggles now.
|
||||
'view.toggleSidebar': toggleSidebarOpen,
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ export const ar = defineLocale({
|
|||
'session.focusSearch': 'البحث في الجلسات',
|
||||
'session.togglePin': 'تثبيت / إلغاء تثبيت الجلسة الحالية',
|
||||
'workspace.newWorktree': 'worktree جديد',
|
||||
'workspace.openFolder': 'فتح مجلد كمشروع',
|
||||
'composer.focus': 'التركيز على المحرّر',
|
||||
'composer.modelPicker': 'فتح منتقي النموذج',
|
||||
'composer.voice': 'بدء / إيقاف المحادثة الصوتية',
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ export const KEYBIND_ACTIONS: readonly KeybindActionMeta[] = [
|
|||
{ id: 'session.togglePin', category: 'session', defaults: [] },
|
||||
// ⌘⇧B — "b" for branch: spin up a new git worktree from the active repo.
|
||||
{ id: 'workspace.newWorktree', category: 'session', defaults: ['mod+shift+b'] },
|
||||
// ⌘O — the editor-standard "open folder" chord (VS Code ⌘O, Zed's
|
||||
// workspace::Open). Picks a folder and opens it as a project (upsert:
|
||||
// enters the owning project when one exists, else creates one), landing on
|
||||
// a fresh session anchored there.
|
||||
{ id: 'workspace.openFolder', category: 'session', defaults: ['mod+o'] },
|
||||
|
||||
// ── Navigation ───────────────────────────────────────────────────────────
|
||||
{ id: 'nav.commandPalette', category: 'navigation', defaults: ['mod+k', 'mod+p'] },
|
||||
|
|
|
|||
|
|
@ -161,6 +161,36 @@ export function exitProjectScope(): void {
|
|||
$projectScope.set(ALL_PROJECTS)
|
||||
}
|
||||
|
||||
// A project's working root: its primary folder, else the first repo that has
|
||||
// one. Empty for the path-less Home bucket. (The sidebar's `projectTreeCwd` is
|
||||
// the same rule over the same tree — this is the store-side copy so the store
|
||||
// doesn't reach into the sidebar's React module.)
|
||||
const projectRootCwd = (project: SidebarProjectTree | undefined): string =>
|
||||
(project?.path || project?.repos.find(repo => repo.path)?.path || '').trim()
|
||||
|
||||
// ⌘K "go to project": flip the sidebar into grouped mode and enter the project
|
||||
// — a pure scope switch, same as clicking the overview row (never spends main).
|
||||
// With `newSession` (⌘-select / ⌘-Enter) it also lands on a fresh session draft
|
||||
// anchored at the project root — stacked as a tab when main already holds a
|
||||
// chat (palette opens are opens-from-nowhere). A path-less project (the Home
|
||||
// bucket) gets a plain detached draft.
|
||||
export function goToProject(id: string, options?: { newSession?: boolean }): void {
|
||||
setSidebarAgentsGrouped(true)
|
||||
enterProject(id)
|
||||
|
||||
if (!options?.newSession) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = projectRootCwd($projectTree.get().find(node => node.id === id))
|
||||
|
||||
if (cwd) {
|
||||
requestStartWorkSession(cwd, undefined, { openTab: true })
|
||||
} else {
|
||||
requestFreshSession()
|
||||
}
|
||||
}
|
||||
|
||||
// The cwd a NEW chat should start in. The "active project" is just an atom
|
||||
// ($projectScope) — so when you're inside a project, a new session (cmd-n, the
|
||||
// trunk "+") starts at that project's root (its primary repo = the default-branch
|
||||
|
|
@ -177,8 +207,7 @@ export function resolveNewSessionCwd(): string {
|
|||
}
|
||||
|
||||
if (scope !== ALL_PROJECTS) {
|
||||
const project = $projectTree.get().find(node => node.id === scope)
|
||||
const cwd = (project?.path || project?.repos.find(repo => repo.path)?.path || '').trim()
|
||||
const cwd = projectRootCwd($projectTree.get().find(node => node.id === scope))
|
||||
|
||||
if (cwd) {
|
||||
return cwd
|
||||
|
|
@ -997,6 +1026,8 @@ export async function switchBranchInRepo(repoPath: string, branch: string): Prom
|
|||
// effect even if the path repeats.
|
||||
export interface StartWorkSessionRequest {
|
||||
draft?: string
|
||||
/** Stack the fresh session as a tab when main already holds a chat (palette/⌘O opens-from-nowhere). */
|
||||
openTab?: boolean
|
||||
path: string
|
||||
token: number
|
||||
}
|
||||
|
|
@ -1016,7 +1047,7 @@ export function requestNewWorktree(): void {
|
|||
|
||||
let startWorkToken = 0
|
||||
|
||||
export function requestStartWorkSession(path: string, draft?: string): void {
|
||||
export function requestStartWorkSession(path: string, draft?: string, options?: { openTab?: boolean }): void {
|
||||
const target = path.trim()
|
||||
|
||||
if (!target) {
|
||||
|
|
@ -1024,7 +1055,12 @@ export function requestStartWorkSession(path: string, draft?: string): void {
|
|||
}
|
||||
|
||||
startWorkToken += 1
|
||||
$startWorkSessionRequest.set({ draft: draft?.trim() || undefined, path: target, token: startWorkToken })
|
||||
$startWorkSessionRequest.set({
|
||||
draft: draft?.trim() || undefined,
|
||||
openTab: options?.openTab || undefined,
|
||||
path: target,
|
||||
token: startWorkToken
|
||||
})
|
||||
}
|
||||
|
||||
export async function removeWorktreePath(
|
||||
|
|
@ -1068,3 +1104,44 @@ export async function pickProjectFolder(): Promise<null | string> {
|
|||
|
||||
return dir || null
|
||||
}
|
||||
|
||||
// ⌘O / palette "Open folder…": open a folder AS a project, upserting. A folder
|
||||
// already covered by a project (explicit or auto) just enters it; anything else
|
||||
// becomes a new project named after the folder. Either way the sidebar scopes
|
||||
// to the project and a fresh session draft lands anchored at the folder — the
|
||||
// one-keystroke version of new project → enter → new session. Like goToProject,
|
||||
// this is an open-from-nowhere: an occupied main gets a stacked tab, not stolen.
|
||||
export async function openFolderAsProject(dir?: string): Promise<void> {
|
||||
const target = (dir ?? (await pickProjectFolder()) ?? '').trim()
|
||||
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh first so the membership check runs against live truth — a repo
|
||||
// cloned since the last scan should enter its auto project, not double-create.
|
||||
await refreshProjectTree()
|
||||
|
||||
const existing = projectIdForCwd(target)
|
||||
|
||||
if (existing) {
|
||||
setSidebarAgentsGrouped(true)
|
||||
enterProject(existing)
|
||||
} else {
|
||||
const name = target.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || target
|
||||
|
||||
try {
|
||||
const created = await createProject({ name, folders: [target], primaryPath: target, use: true })
|
||||
|
||||
if (created) {
|
||||
enterProject(created.id)
|
||||
}
|
||||
} catch (err) {
|
||||
// Stale backend (no projects.* RPC) or a failed write: still open the
|
||||
// folder as a plain workspace session below — the project row can wait.
|
||||
notify({ kind: 'warning', message: err instanceof Error ? err.message : String(err) })
|
||||
}
|
||||
}
|
||||
|
||||
requestStartWorkSession(target, undefined, { openTab: true })
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue