mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
feat: restructure new-workspace flow and add Tasks sidebar nav
Split the "new workspace" and "tasks" entry points: the Plus button and Cmd/Ctrl+N
now open the lightweight composer modal, while the Tasks page is reached via a
dedicated Codex-style button above the Workspaces header in the sidebar.
- Add SidebarNav with a Tasks button (GH + Linear icons on the right)
- NewWorkspacePage becomes a pure tasks list (drops embedded composer card)
- Landing, AddRepoDialog, WorktreeList, SidebarHeader, App shortcut all switched
to openModal('new-workspace-composer')
- Normalize SidebarHeader row height so it matches the Tasks button above
This commit is contained in:
parent
481bbeb985
commit
2c3713ab85
8 changed files with 150 additions and 135 deletions
|
|
@ -91,8 +91,7 @@ function App(): React.JSX.Element {
|
|||
toggleRightSidebar: s.toggleRightSidebar,
|
||||
setRightSidebarOpen: s.setRightSidebarOpen,
|
||||
setRightSidebarTab: s.setRightSidebarTab,
|
||||
updateSettings: s.updateSettings,
|
||||
openNewWorkspacePage: s.openNewWorkspacePage
|
||||
updateSettings: s.updateSettings
|
||||
}))
|
||||
)
|
||||
|
||||
|
|
@ -523,14 +522,14 @@ function App(): React.JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
// Cmd/Ctrl+N — new workspace
|
||||
// Cmd/Ctrl+N — new workspace (opens the lightweight composer modal)
|
||||
if (!e.altKey && !e.shiftKey && e.key.toLowerCase() === 'n') {
|
||||
if (!repos.some((repo) => isGitRepoKind(repo))) {
|
||||
return
|
||||
}
|
||||
dispatchClearModifierHints()
|
||||
e.preventDefault()
|
||||
actions.openNewWorkspacePage()
|
||||
actions.openModal('new-workspace-composer')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { AlertTriangle, ExternalLink, FolderPlus, GitBranchPlus, Star } from 'lu
|
|||
import { cn } from '../lib/utils'
|
||||
import { useAppStore } from '../store'
|
||||
import { isGitRepoKind } from '../../../shared/repo-kind'
|
||||
import { getTaskPresetQuery } from '../lib/new-workspace'
|
||||
import { ShortcutKeyCombo } from './ShortcutKeyCombo'
|
||||
import logo from '../../../../resources/logo.svg'
|
||||
|
||||
|
|
@ -149,26 +148,10 @@ function PreflightBanner({ issues }: { issues: PreflightIssue[] }): React.JSX.El
|
|||
|
||||
export default function Landing(): React.JSX.Element {
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const openNewWorkspacePage = useAppStore((s) => s.openNewWorkspacePage)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const prefetchWorkItems = useAppStore((s) => s.prefetchWorkItems)
|
||||
const defaultTaskViewPreset = useAppStore((s) => s.settings?.defaultTaskViewPreset ?? 'all')
|
||||
|
||||
const canCreateWorktree = repos.some((repo) => isGitRepoKind(repo))
|
||||
|
||||
// Why: warm the exact cache key NewWorkspacePage will read on mount — the
|
||||
// default-preset query must match or the page pays a full round-trip after
|
||||
// click.
|
||||
const handlePrefetchNewWorkspace = (): void => {
|
||||
if (!canCreateWorktree) {
|
||||
return
|
||||
}
|
||||
const firstGit = repos.find((r) => isGitRepoKind(r))
|
||||
if (firstGit?.path) {
|
||||
prefetchWorkItems(firstGit.path, 36, getTaskPresetQuery(defaultTaskViewPreset))
|
||||
}
|
||||
}
|
||||
|
||||
const [preflightIssues, setPreflightIssues] = useState<PreflightIssue[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -264,9 +247,7 @@ export default function Landing(): React.JSX.Element {
|
|||
className="inline-flex items-center gap-1.5 bg-secondary/70 border border-border/80 text-foreground font-medium text-sm px-4 py-2 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed enabled:cursor-pointer enabled:hover:bg-accent"
|
||||
disabled={!canCreateWorktree}
|
||||
title={!canCreateWorktree ? 'Add a Git repo first' : undefined}
|
||||
onClick={() => openNewWorkspacePage()}
|
||||
onPointerEnter={handlePrefetchNewWorkspace}
|
||||
onFocus={handlePrefetchNewWorkspace}
|
||||
onClick={() => openModal('new-workspace-composer')}
|
||||
>
|
||||
<GitBranchPlus className="size-3.5" />
|
||||
Create Worktree
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable max-lines -- Why: the new-workspace page keeps the composer,
|
||||
/* eslint-disable max-lines -- Why: the tasks page keeps the repo selector,
|
||||
task source controls, and GitHub task list co-located so the wiring between the
|
||||
selected repo, the draft composer, and the work-item list stays readable in one
|
||||
selected repo, the task filters, and the work-item list stays readable in one
|
||||
place while this surface is still evolving. */
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
|
|
@ -27,13 +27,12 @@ import {
|
|||
} from '@/components/ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import RepoCombobox from '@/components/repo/RepoCombobox'
|
||||
import NewWorkspaceComposerCard from '@/components/NewWorkspaceComposerCard'
|
||||
import GitHubItemDrawer from '@/components/GitHubItemDrawer'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { LightRays } from '@/components/ui/light-rays'
|
||||
import { useComposerState } from '@/hooks/useComposerState'
|
||||
import { getLinkedWorkItemSuggestedName, getTaskPresetQuery } from '@/lib/new-workspace'
|
||||
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
|
||||
import { isGitRepoKind } from '../../../shared/repo-kind'
|
||||
import type { GitHubWorkItem, TaskViewPresetId } from '../../../shared/types'
|
||||
|
||||
type TaskSource = 'github' | 'linear'
|
||||
|
|
@ -134,24 +133,43 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
const settings = useAppStore((s) => s.settings)
|
||||
const pageData = useAppStore((s) => s.newWorkspacePageData)
|
||||
const closeNewWorkspacePage = useAppStore((s) => s.closeNewWorkspacePage)
|
||||
const clearNewWorkspaceDraft = useAppStore((s) => s.clearNewWorkspaceDraft)
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const activeRepoId = useAppStore((s) => s.activeRepoId)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const updateSettings = useAppStore((s) => s.updateSettings)
|
||||
const fetchWorkItems = useAppStore((s) => s.fetchWorkItems)
|
||||
const getCachedWorkItems = useAppStore((s) => s.getCachedWorkItems)
|
||||
|
||||
const { cardProps, composerRef, promptTextareaRef, submit, createDisabled } = useComposerState({
|
||||
persistDraft: true,
|
||||
initialRepoId: pageData.preselectedRepoId,
|
||||
initialName: pageData.prefilledName,
|
||||
onCreated: () => {
|
||||
clearNewWorkspaceDraft()
|
||||
closeNewWorkspacePage()
|
||||
}
|
||||
})
|
||||
const eligibleRepos = useMemo(() => repos.filter((repo) => isGitRepoKind(repo)), [repos])
|
||||
|
||||
// Why: resolve the initial repo from (1) explicit page data, (2) the app's
|
||||
// currently active repo, (3) the first eligible repo. Falls back to '' so
|
||||
// RepoCombobox renders its placeholder until the user picks one.
|
||||
const resolvedInitialRepoId = useMemo(() => {
|
||||
const preferred = pageData.preselectedRepoId
|
||||
if (preferred && eligibleRepos.some((repo) => repo.id === preferred)) {
|
||||
return preferred
|
||||
}
|
||||
if (activeRepoId && eligibleRepos.some((repo) => repo.id === activeRepoId)) {
|
||||
return activeRepoId
|
||||
}
|
||||
return eligibleRepos[0]?.id ?? ''
|
||||
}, [activeRepoId, eligibleRepos, pageData.preselectedRepoId])
|
||||
|
||||
const [repoId, setRepoId] = useState<string>(resolvedInitialRepoId)
|
||||
|
||||
// Why: if the repo list changes such that the current repoId is no longer
|
||||
// eligible (e.g. repo removed), fall back to a valid one.
|
||||
useEffect(() => {
|
||||
if (!repoId && eligibleRepos[0]?.id) {
|
||||
setRepoId(eligibleRepos[0].id)
|
||||
return
|
||||
}
|
||||
if (repoId && !eligibleRepos.some((repo) => repo.id === repoId)) {
|
||||
setRepoId(eligibleRepos[0]?.id ?? '')
|
||||
}
|
||||
}, [eligibleRepos, repoId])
|
||||
|
||||
const { repoId, eligibleRepos, onRepoChange } = cardProps
|
||||
const selectedRepo = eligibleRepos.find((repo) => repo.id === repoId)
|
||||
|
||||
// Why: seed the preset + query from the user's saved default synchronously
|
||||
|
|
@ -214,11 +232,6 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
})
|
||||
}, [activeTaskPreset, workItems])
|
||||
|
||||
// Autofocus prompt on mount so the user can start typing immediately.
|
||||
useEffect(() => {
|
||||
promptTextareaRef.current?.focus()
|
||||
}, [promptTextareaRef])
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
setAppliedTaskSearch(taskSearchInput)
|
||||
|
|
@ -346,29 +359,17 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
[openModal, repoId]
|
||||
)
|
||||
|
||||
const handleDiscardDraft = useCallback((): void => {
|
||||
clearNewWorkspaceDraft()
|
||||
closeNewWorkspacePage()
|
||||
}, [clearNewWorkspaceDraft, closeNewWorkspacePage])
|
||||
|
||||
useEffect(() => {
|
||||
// Why: when the global composer modal is on top, let its own scoped key
|
||||
// handler own Enter/Esc so we don't double-fire (e.g. modal Esc closes
|
||||
// itself *and* this handler tries to discard the underlying page draft).
|
||||
if (activeModal === 'new-workspace-composer') {
|
||||
return
|
||||
}
|
||||
|
||||
// Why: when the GitHub preview sheet is open, Radix's Dialog owns Esc —
|
||||
// it closes the sheet on its own. Page-level capture would otherwise fire
|
||||
// first and discard the whole draft while the user just meant to dismiss
|
||||
// the preview.
|
||||
// first and pop the tasks page while the user just meant to dismiss the
|
||||
// preview.
|
||||
if (drawerWorkItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key !== 'Enter' && event.key !== 'Escape') {
|
||||
if (event.key !== 'Escape') {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -377,45 +378,27 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
// Why: Esc should first dismiss the focused control so users can back
|
||||
// out of text entry without accidentally closing the whole composer.
|
||||
// Once focus is already outside an input, Esc becomes the discard shortcut.
|
||||
if (
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
event.preventDefault()
|
||||
target.blur()
|
||||
return
|
||||
}
|
||||
|
||||
// Why: Esc should first dismiss the focused control so users can back
|
||||
// out of text entry without accidentally closing the whole page.
|
||||
// Once focus is already outside an input, Esc closes the tasks page.
|
||||
if (
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
event.preventDefault()
|
||||
handleDiscardDraft()
|
||||
return
|
||||
}
|
||||
|
||||
if (!composerRef.current?.contains(target)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (createDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (target instanceof HTMLTextAreaElement && event.shiftKey) {
|
||||
target.blur()
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
void submit()
|
||||
closeNewWorkspacePage()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true })
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
|
||||
}, [activeModal, composerRef, createDisabled, drawerWorkItem, handleDiscardDraft, submit])
|
||||
}, [closeNewWorkspacePage, drawerWorkItem])
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full min-h-0 flex-1 overflow-hidden bg-background dark:bg-[#1a1a1a] text-foreground">
|
||||
|
|
@ -452,24 +435,20 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 rounded-full z-10"
|
||||
onClick={handleDiscardDraft}
|
||||
aria-label="Discard draft and go back"
|
||||
onClick={closeNewWorkspacePage}
|
||||
aria-label="Close tasks"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
Discard draft · Esc
|
||||
Close · Esc
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto flex w-full max-w-[1120px] flex-1 flex-col min-h-0 px-5 pb-5 md:px-8 md:pb-7">
|
||||
<div className="flex-none flex flex-col gap-5">
|
||||
<section className="mx-auto w-full max-w-[860px] border-b border-border/50 pb-5">
|
||||
<NewWorkspaceComposerCard composerRef={composerRef} {...cardProps} />
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -506,7 +485,7 @@ export default function NewWorkspacePage(): React.JSX.Element {
|
|||
<RepoCombobox
|
||||
repos={eligibleRepos}
|
||||
value={repoId}
|
||||
onValueChange={onRepoChange}
|
||||
onValueChange={setRepoId}
|
||||
placeholder="Select a repository"
|
||||
triggerClassName="h-11 w-full rounded-[10px] border border-border/50 bg-background/50 backdrop-blur-md px-3 text-sm font-medium shadow-sm transition hover:bg-muted/50 focus:ring-2 focus:ring-ring/20 focus:outline-none supports-[backdrop-filter]:bg-background/50"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
|||
const repos = useAppStore((s) => s.repos)
|
||||
const worktreesByRepo = useAppStore((s) => s.worktreesByRepo)
|
||||
const fetchWorktrees = useAppStore((s) => s.fetchWorktrees)
|
||||
const openNewWorkspacePage = useAppStore((s) => s.openNewWorkspacePage)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
|
||||
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
|
||||
|
||||
|
|
@ -185,14 +185,14 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
|||
)
|
||||
|
||||
const handleCreateWorktree = useCallback(() => {
|
||||
// Why: small delay so the Add Repo dialog close animation finishes before
|
||||
// the composer modal takes focus; otherwise the dialog teardown can steal
|
||||
// the first focus frame from the composer's prompt textarea.
|
||||
closeModal()
|
||||
// Why: small delay so the close animation finishes before the full-page create
|
||||
// view takes focus; otherwise the dialog teardown can steal the first focus
|
||||
// frame from the workspace form.
|
||||
setTimeout(() => {
|
||||
openNewWorkspacePage({ preselectedRepoId: repoId })
|
||||
openModal('new-workspace-composer', { initialRepoId: repoId })
|
||||
}, 150)
|
||||
}, [closeModal, openNewWorkspacePage, repoId])
|
||||
}, [closeModal, openModal, repoId])
|
||||
|
||||
const handleConfigureRepo = useCallback(() => {
|
||||
closeModal()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useAppStore } from '@/store'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
import { getTaskPresetQuery } from '@/lib/new-workspace'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -37,7 +36,7 @@ const isMac = navigator.userAgent.includes('Mac')
|
|||
const newWorktreeShortcutLabel = isMac ? '⌘N' : 'Ctrl+N'
|
||||
|
||||
const SidebarHeader = React.memo(function SidebarHeader() {
|
||||
const openNewWorkspacePage = useAppStore((s) => s.openNewWorkspacePage)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const canCreateWorktree = repos.some((repo) => isGitRepoKind(repo))
|
||||
|
||||
|
|
@ -46,29 +45,8 @@ const SidebarHeader = React.memo(function SidebarHeader() {
|
|||
const sortBy = useAppStore((s) => s.sortBy)
|
||||
const setSortBy = useAppStore((s) => s.setSortBy)
|
||||
|
||||
// Why: start warming the GitHub work-item cache on hover/focus/pointerdown so
|
||||
// by the time the user's click finishes the round-trip has either completed
|
||||
// or is already in-flight. Shaves ~200–600ms off perceived page-load latency.
|
||||
const prefetchWorkItems = useAppStore((s) => s.prefetchWorkItems)
|
||||
const activeRepoId = useAppStore((s) => s.activeRepoId)
|
||||
const defaultTaskViewPreset = useAppStore((s) => s.settings?.defaultTaskViewPreset ?? 'all')
|
||||
const handlePrefetch = React.useCallback(() => {
|
||||
if (!canCreateWorktree) {
|
||||
return
|
||||
}
|
||||
const activeRepo = repos.find((r) => r.id === activeRepoId && isGitRepoKind(r))
|
||||
const firstGitRepo = activeRepo ?? repos.find((r) => isGitRepoKind(r))
|
||||
if (firstGitRepo?.path) {
|
||||
// Why: warm the exact cache key the page will read on mount — must
|
||||
// match NewWorkspacePage's `initialTaskQuery` derived from the same
|
||||
// default preset, otherwise the prefetch lands in a key the page
|
||||
// never reads and we pay the full round-trip after click.
|
||||
prefetchWorkItems(firstGitRepo.path, 36, getTaskPresetQuery(defaultTaskViewPreset))
|
||||
}
|
||||
}, [activeRepoId, canCreateWorktree, defaultTaskViewPreset, prefetchWorkItems, repos])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 pt-3 pb-1">
|
||||
<div className="flex h-8 items-center justify-between px-4 mt-1">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground select-none">
|
||||
Workspaces
|
||||
</span>
|
||||
|
|
@ -134,11 +112,9 @@ const SidebarHeader = React.memo(function SidebarHeader() {
|
|||
if (!canCreateWorktree) {
|
||||
return
|
||||
}
|
||||
openNewWorkspacePage()
|
||||
openModal('new-workspace-composer')
|
||||
}}
|
||||
onPointerEnter={handlePrefetch}
|
||||
onFocus={handlePrefetch}
|
||||
aria-label="Add worktree"
|
||||
aria-label="New workspace"
|
||||
disabled={!canCreateWorktree}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
|
|
|
|||
78
src/renderer/src/components/sidebar/SidebarNav.tsx
Normal file
78
src/renderer/src/components/sidebar/SidebarNav.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import React from 'react'
|
||||
import { Github, ListChecks } from 'lucide-react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
import { getTaskPresetQuery } from '@/lib/new-workspace'
|
||||
|
||||
function LinearIcon({ className }: { className?: string }): React.JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden className={className} fill="currentColor">
|
||||
<path d="M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarNav = React.memo(function SidebarNav() {
|
||||
const openNewWorkspacePage = useAppStore((s) => s.openNewWorkspacePage)
|
||||
const activeView = useAppStore((s) => s.activeView)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const canBrowseTasks = repos.some((repo) => isGitRepoKind(repo))
|
||||
|
||||
// Why: warm the GitHub work-item cache on hover/focus so by the time the
|
||||
// user's click finishes the round-trip has either completed or is already
|
||||
// in-flight. Shaves ~200–600ms off perceived page-load latency.
|
||||
const prefetchWorkItems = useAppStore((s) => s.prefetchWorkItems)
|
||||
const activeRepoId = useAppStore((s) => s.activeRepoId)
|
||||
const defaultTaskViewPreset = useAppStore((s) => s.settings?.defaultTaskViewPreset ?? 'all')
|
||||
const handlePrefetch = React.useCallback(() => {
|
||||
if (!canBrowseTasks) {
|
||||
return
|
||||
}
|
||||
const activeRepo = repos.find((r) => r.id === activeRepoId && isGitRepoKind(r))
|
||||
const firstGitRepo = activeRepo ?? repos.find((r) => isGitRepoKind(r))
|
||||
if (firstGitRepo?.path) {
|
||||
// Why: warm the exact cache key the page will read on mount — must
|
||||
// match NewWorkspacePage's `initialTaskQuery` derived from the same
|
||||
// default preset, otherwise the prefetch lands in a key the page
|
||||
// never reads and we pay the full round-trip after click.
|
||||
prefetchWorkItems(firstGitRepo.path, 36, getTaskPresetQuery(defaultTaskViewPreset))
|
||||
}
|
||||
}, [activeRepoId, canBrowseTasks, defaultTaskViewPreset, prefetchWorkItems, repos])
|
||||
|
||||
const tasksActive = activeView === 'new-workspace'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 px-2 pt-2 pb-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!canBrowseTasks) {
|
||||
return
|
||||
}
|
||||
openNewWorkspacePage()
|
||||
}}
|
||||
onPointerEnter={handlePrefetch}
|
||||
onFocus={handlePrefetch}
|
||||
disabled={!canBrowseTasks}
|
||||
aria-current={tasksActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors',
|
||||
tasksActive
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/60 hover:text-sidebar-accent-foreground',
|
||||
!canBrowseTasks && 'cursor-not-allowed opacity-50 hover:bg-transparent'
|
||||
)}
|
||||
>
|
||||
<ListChecks className="size-4 shrink-0" />
|
||||
<span className="flex-1">Tasks</span>
|
||||
<span className="flex items-center gap-1 text-muted-foreground/70">
|
||||
<Github className="size-3.5" aria-hidden />
|
||||
<LinearIcon className="size-3.5" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default SidebarNav
|
||||
|
|
@ -404,7 +404,7 @@ const WorktreeList = React.memo(function WorktreeList() {
|
|||
const sortBy = useAppStore((s) => s.sortBy)
|
||||
const showActiveOnly = useAppStore((s) => s.showActiveOnly)
|
||||
const filterRepoIds = useAppStore((s) => s.filterRepoIds)
|
||||
const openNewWorkspacePage = useAppStore((s) => s.openNewWorkspacePage)
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const activeView = useAppStore((s) => s.activeView)
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
const pendingRevealWorktreeId = useAppStore((s) => s.pendingRevealWorktreeId)
|
||||
|
|
@ -657,9 +657,9 @@ const WorktreeList = React.memo(function WorktreeList() {
|
|||
|
||||
const handleCreateForRepo = useCallback(
|
||||
(repoId: string) => {
|
||||
openNewWorkspacePage({ preselectedRepoId: repoId })
|
||||
openModal('new-workspace-composer', { initialRepoId: repoId })
|
||||
},
|
||||
[openNewWorkspacePage]
|
||||
[openModal]
|
||||
)
|
||||
|
||||
const hasFilters = !!(searchQuery || showActiveOnly || filterRepoIds.length)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { cn } from '@/lib/utils'
|
|||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { useSidebarResize } from '@/hooks/useSidebarResize'
|
||||
import SidebarHeader from './SidebarHeader'
|
||||
import SidebarNav from './SidebarNav'
|
||||
import SearchBar from './SearchBar'
|
||||
import GroupControls from './GroupControls'
|
||||
import WorktreeList from './WorktreeList'
|
||||
|
|
@ -51,6 +52,7 @@ function Sidebar(): React.JSX.Element {
|
|||
)}
|
||||
>
|
||||
{/* Fixed controls */}
|
||||
<SidebarNav />
|
||||
<SidebarHeader />
|
||||
<SearchBar />
|
||||
<GroupControls />
|
||||
|
|
|
|||
Loading…
Reference in a new issue