|
|
|
@@ -72,47 +72,6 @@ interface Option<T extends string = string> {
|
|
|
|
|
label: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const GROUPINGS: Option<SidebarGrouping>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'date', label: 'Updated' },
|
|
|
|
|
{ icon: 'root-folder', id: 'project', label: 'Project' },
|
|
|
|
|
{ icon: 'pulse', id: 'status', label: 'Status' },
|
|
|
|
|
{ icon: 'account', id: 'profile', label: 'Profile' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const ORDERINGS: Option<SidebarOrdering>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'updated', label: 'Updated' },
|
|
|
|
|
{ icon: 'add', id: 'created', label: 'Created' },
|
|
|
|
|
{ icon: 'pulse', id: 'status', label: 'Status' },
|
|
|
|
|
{ icon: 'symbol-numeric', id: 'tokens', label: 'Tokens' },
|
|
|
|
|
{ icon: 'credit-card', id: 'cost', label: 'Cost' },
|
|
|
|
|
{ icon: 'list-ordered', id: 'manual', label: 'Manual' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const ROW_META: Option<SidebarRowMeta>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'updated', label: 'Updated' },
|
|
|
|
|
{ icon: 'comment', id: 'preview', label: 'Preview' },
|
|
|
|
|
{ icon: 'symbol-numeric', id: 'tokens', label: 'Tokens' },
|
|
|
|
|
{ icon: 'credit-card', id: 'cost', label: 'Cost' },
|
|
|
|
|
{ icon: 'git-pull-request', id: 'pr', label: 'PR' },
|
|
|
|
|
{ icon: 'account', id: 'profile', label: 'Profile' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const PR_FILTERS: Option<PullRequestBucket>[] = [
|
|
|
|
|
{ icon: 'git-pull-request', id: 'open', label: 'Open' },
|
|
|
|
|
{ icon: 'git-pull-request-draft', id: 'draft', label: 'Draft' },
|
|
|
|
|
{ icon: 'git-merge', id: 'merged', label: 'Merged' },
|
|
|
|
|
{ icon: 'git-pull-request-closed', id: 'closed', label: 'Closed' },
|
|
|
|
|
{ icon: 'circle-slash', id: 'none', label: 'No PR' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const STATUS_FILTERS: Option<SessionStatusBucket>[] = [
|
|
|
|
|
{ dot: sessionDotClassName('needs-input'), id: 'needs-input', label: 'Needs input' },
|
|
|
|
|
{ dot: sessionDotClassName('working'), id: 'working', label: 'Working' },
|
|
|
|
|
{ dot: sessionDotClassName('unread'), id: 'unread', label: 'Unread' },
|
|
|
|
|
{ dot: sessionDotClassName('draft'), id: 'draft', label: 'Draft' },
|
|
|
|
|
{ dot: cn(sessionDotClassName('idle'), 'bg-(--ui-text-quaternary)'), id: 'idle', label: 'Idle' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function OptionGlyph({ option }: { option: Option }) {
|
|
|
|
|
if (option.dot) {
|
|
|
|
|
return <span aria-hidden="true" className={cn('shrink-0', option.dot)} />
|
|
|
|
@@ -121,7 +80,7 @@ function OptionGlyph({ option }: { option: Option }) {
|
|
|
|
|
return option.icon ? <Codicon className="text-(--ui-text-tertiary)" name={option.icon} size="0.8125rem" /> : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Every option row — single or multi select — leaves the menu open, so a whole
|
|
|
|
|
/** Every option row — single or multi select — leaves the menu open, so a whole
|
|
|
|
|
* view can be set up in one pass. Only the actions at the bottom dismiss it. */
|
|
|
|
|
const keepOpen = (event: Event) => event.preventDefault()
|
|
|
|
|
|
|
|
|
@@ -151,6 +110,48 @@ function OptionRadio({ option }: { option: Option }) {
|
|
|
|
|
|
|
|
|
|
export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
const { t } = useI18n()
|
|
|
|
|
|
|
|
|
|
const GROUPINGS: Option<SidebarGrouping>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'date', label: t.surfaces.filters.updated },
|
|
|
|
|
{ icon: 'root-folder', id: 'project', label: t.surfaces.filters.project },
|
|
|
|
|
{ icon: 'pulse', id: 'status', label: t.surfaces.filters.status },
|
|
|
|
|
{ icon: 'account', id: 'profile', label: t.surfaces.filters.profile }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const ORDERINGS: Option<SidebarOrdering>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'updated', label: t.surfaces.filters.updated },
|
|
|
|
|
{ icon: 'add', id: 'created', label: t.surfaces.filters.created },
|
|
|
|
|
{ icon: 'pulse', id: 'status', label: t.surfaces.filters.status },
|
|
|
|
|
{ icon: 'symbol-numeric', id: 'tokens', label: t.surfaces.filters.tokens },
|
|
|
|
|
{ icon: 'credit-card', id: 'cost', label: t.surfaces.filters.cost },
|
|
|
|
|
{ icon: 'list-ordered', id: 'manual', label: t.surfaces.filters.manual }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const ROW_META: Option<SidebarRowMeta>[] = [
|
|
|
|
|
{ icon: 'clock', id: 'updated', label: t.surfaces.filters.updated },
|
|
|
|
|
{ icon: 'comment', id: 'preview', label: t.surfaces.filters.preview },
|
|
|
|
|
{ icon: 'symbol-numeric', id: 'tokens', label: t.surfaces.filters.tokens },
|
|
|
|
|
{ icon: 'credit-card', id: 'cost', label: t.surfaces.filters.cost },
|
|
|
|
|
{ icon: 'git-pull-request', id: 'pr', label: t.surfaces.filters.pr },
|
|
|
|
|
{ icon: 'account', id: 'profile', label: t.surfaces.filters.profile }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const PR_FILTERS: Option<PullRequestBucket>[] = [
|
|
|
|
|
{ icon: 'git-pull-request', id: 'open', label: t.surfaces.filters.open },
|
|
|
|
|
{ icon: 'git-pull-request-draft', id: 'draft', label: t.surfaces.filters.draft },
|
|
|
|
|
{ icon: 'git-merge', id: 'merged', label: t.surfaces.filters.merged },
|
|
|
|
|
{ icon: 'git-pull-request-closed', id: 'closed', label: t.surfaces.filters.closed },
|
|
|
|
|
{ icon: 'circle-slash', id: 'none', label: t.surfaces.filters.noPr }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const STATUS_FILTERS: Option<SessionStatusBucket>[] = [
|
|
|
|
|
{ dot: sessionDotClassName('needs-input'), id: 'needs-input', label: t.surfaces.filters.needsInput },
|
|
|
|
|
{ dot: sessionDotClassName('working'), id: 'working', label: t.surfaces.filters.working },
|
|
|
|
|
{ dot: sessionDotClassName('unread'), id: 'unread', label: t.surfaces.filters.unread },
|
|
|
|
|
{ dot: sessionDotClassName('draft'), id: 'draft', label: t.surfaces.filters.draft },
|
|
|
|
|
{ dot: cn(sessionDotClassName('idle'), 'bg-(--ui-text-quaternary)'), id: 'idle', label: t.surfaces.filters.idle }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const grouping = useStore($sidebarGrouping)
|
|
|
|
|
const ordering = useStore($sidebarOrdering)
|
|
|
|
|
const rowMeta = useStore($sidebarRowMeta)
|
|
|
|
@@ -170,7 +171,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
const projects = useStore($projectTree)
|
|
|
|
|
const hasCost = useStore($sessionsHaveCost)
|
|
|
|
|
const unreadIds = useStore($unreadFinishedSessionIds)
|
|
|
|
|
// PR state comes from `gh` on whichever machine holds the checkout — Electron
|
|
|
|
|
// PR state comes from `gh` on whichever machine holds the checkout — Electron
|
|
|
|
|
// locally, the gateway's REST mirror remotely. Resolved per render, not once
|
|
|
|
|
// at module load: switching to a remote profile swaps the bridge underneath.
|
|
|
|
|
const prAvailable = Boolean(desktopGit()?.review?.prList)
|
|
|
|
@@ -217,12 +218,12 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
aria-label="Filters"
|
|
|
|
|
aria-label={t.surfaces.filters.filters}
|
|
|
|
|
className={cn(
|
|
|
|
|
className,
|
|
|
|
|
'data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100',
|
|
|
|
|
// Active filters read as "this control is engaged", the same way the
|
|
|
|
|
// open menu does — never as an accent, which the sidebar reserves
|
|
|
|
|
// open menu does — never as an accent, which the sidebar reserves
|
|
|
|
|
// for a session that is actually doing something.
|
|
|
|
|
filtersActive && 'bg-(--ui-control-active-background) text-foreground opacity-100'
|
|
|
|
|
)}
|
|
|
|
@@ -238,7 +239,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
<DropdownMenuGroup>
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger hideChevron>
|
|
|
|
|
Grouping
|
|
|
|
|
{t.surfaces.filters.grouping}
|
|
|
|
|
<span className="ml-auto flex items-center gap-1 pl-4 text-(--ui-text-tertiary)">
|
|
|
|
|
{groupingLabel}
|
|
|
|
|
<Codicon name="chevron-right" size="1rem" />
|
|
|
|
@@ -257,7 +258,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
</DropdownMenuSub>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Ordering</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.ordering}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent>
|
|
|
|
|
<DropdownMenuRadioGroup
|
|
|
|
|
onValueChange={value => setSidebarOrdering(value as SidebarOrdering)}
|
|
|
|
@@ -271,7 +272,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
</DropdownMenuSub>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Show</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.show}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent>
|
|
|
|
|
{rowMetaOptions.map(option => (
|
|
|
|
|
<OptionCheckbox
|
|
|
|
@@ -284,22 +285,22 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
</DropdownMenuSubContent>
|
|
|
|
|
</DropdownMenuSub>
|
|
|
|
|
|
|
|
|
|
{/* A render variant, not a grouping: three-line cards (project · age /
|
|
|
|
|
title / model · size) compose with whichever grouping is active. */}
|
|
|
|
|
{/* A render variant, not a grouping: three-line cards (project · age /
|
|
|
|
|
title / model · size) compose with whichever grouping is active. */}
|
|
|
|
|
<OptionCheckbox
|
|
|
|
|
checked={cardRows}
|
|
|
|
|
onCheck={() => setSidebarCardRows(!cardRows)}
|
|
|
|
|
option={{ icon: 'inbox', id: 'card-rows', label: 'Inbox style' }}
|
|
|
|
|
option={{ icon: 'inbox', id: 'card-rows', label: t.surfaces.filters.inbox }}
|
|
|
|
|
/>
|
|
|
|
|
</DropdownMenuGroup>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
|
|
|
|
<DropdownMenuGroup>
|
|
|
|
|
<DropdownMenuLabel>Filters</DropdownMenuLabel>
|
|
|
|
|
<DropdownMenuLabel>{t.surfaces.filters.filters}</DropdownMenuLabel>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Status</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.status}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent>
|
|
|
|
|
{STATUS_FILTERS.map(option => (
|
|
|
|
|
<OptionCheckbox
|
|
|
|
@@ -316,7 +317,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
this submenu never appears rather than filtering everything out. */}
|
|
|
|
|
{prAvailable && (
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Pull request</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.pullRequest}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent>
|
|
|
|
|
{PR_FILTERS.map(option => (
|
|
|
|
|
<OptionCheckbox
|
|
|
|
@@ -331,7 +332,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Profile</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.profile}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent className="max-h-80 overflow-y-auto">
|
|
|
|
|
{/* Scoped to one profile the rail is already the filter, so the
|
|
|
|
|
per-profile boxes only appear where they can narrow something.
|
|
|
|
@@ -358,7 +359,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
|
|
|
|
|
{projects.length > 1 && (
|
|
|
|
|
<DropdownMenuSub>
|
|
|
|
|
<DropdownMenuSubTrigger>Project</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubTrigger>{t.surfaces.filters.project}</DropdownMenuSubTrigger>
|
|
|
|
|
<DropdownMenuSubContent className="max-h-80 overflow-y-auto">
|
|
|
|
|
{projects.map(project => (
|
|
|
|
|
<OptionCheckbox
|
|
|
|
@@ -378,7 +379,7 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Off by default: one profile's sessions are what the rail selected.
|
|
|
|
|
Nothing to widen to until a second profile exists — but stay
|
|
|
|
|
Nothing to widen to until a second profile exists — but stay
|
|
|
|
|
visible while it's on, or deleting your way back down to one
|
|
|
|
|
profile would strand the sidebar in a mode nothing can leave (the
|
|
|
|
|
rail hides its switcher at one profile too). */}
|
|
|
|
@@ -393,23 +394,25 @@ export function SidebarFilterMenu({ className }: { className?: string }) {
|
|
|
|
|
<OptionCheckbox
|
|
|
|
|
checked={showArchived}
|
|
|
|
|
onCheck={() => setSidebarShowArchived(!showArchived)}
|
|
|
|
|
option={{ id: 'archived', label: 'Archived' }}
|
|
|
|
|
option={{ id: 'archived', label: t.surfaces.filters.archived }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* One way back rather than two near-identical ones: this drops the
|
|
|
|
|
grouping and sort too, which "clear filters" left behind. */}
|
|
|
|
|
{viewCustomized && <DropdownMenuItem onSelect={resetSidebarView}>Reset to defaults</DropdownMenuItem>}
|
|
|
|
|
{viewCustomized && (
|
|
|
|
|
<DropdownMenuItem onSelect={resetSidebarView}>{t.surfaces.filters.reset}</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</DropdownMenuGroup>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
|
|
|
|
|
{foldIds.length > 0 && (
|
|
|
|
|
<DropdownMenuItem onSelect={() => setWorkspaceNodesOpen(foldIds, foldCollapsed)}>
|
|
|
|
|
{foldCollapsed ? 'Expand all' : 'Collapse all'}
|
|
|
|
|
{foldCollapsed ? t.surfaces.filters.expand : t.surfaces.filters.collapse}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
<DropdownMenuItem disabled={unreadIds.length === 0} onSelect={markAllSessionsRead}>
|
|
|
|
|
Mark all as read
|
|
|
|
|
{t.surfaces.filters.markRead}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|