mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat: configurable worktree cards with property toggles (#232)
- fix: stay under max-lines lint limit after merge with main Extract restoreScrollbackBuffers into layout-serialization.ts to reduce use-terminal-pane-lifecycle.ts below the 300-line limit. Also filter stale leaf IDs from persisted scrollback buffers and remove design doc. - fix: polish worktree card controls - feat: configurable worktree cards with property toggles
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../shared/constants'
|
||||
|
||||
import { Minimize2, PanelLeft, PanelRight } from 'lucide-react'
|
||||
import { TOGGLE_TERMINAL_PANE_EXPAND_EVENT } from '@/constants/terminal'
|
||||
import { syncZoomCSSVar } from '@/lib/ui-zoom'
|
||||
@@ -100,7 +102,8 @@ function App(): React.JSX.Element {
|
||||
groupBy: 'none',
|
||||
sortBy: 'name',
|
||||
filterRepoIds: [],
|
||||
uiZoomLevel: 0
|
||||
uiZoomLevel: 0,
|
||||
worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES]
|
||||
})
|
||||
hydrateWorkspaceSession({
|
||||
activeRepoId: null,
|
||||
|
||||
@@ -1,41 +1,13 @@
|
||||
import React from 'react'
|
||||
import { ArrowUpAZ, ArrowUpDown, Check, Clock3, FolderTree } from 'lucide-react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
const SORT_OPTIONS = {
|
||||
name: {
|
||||
label: 'Name',
|
||||
icon: ArrowUpAZ
|
||||
},
|
||||
recent: {
|
||||
label: 'Recent',
|
||||
icon: Clock3
|
||||
},
|
||||
repo: {
|
||||
label: 'Repo',
|
||||
icon: FolderTree
|
||||
}
|
||||
} as const
|
||||
|
||||
const GroupControls = React.memo(function GroupControls() {
|
||||
const groupBy = useAppStore((s) => s.groupBy)
|
||||
const setGroupBy = useAppStore((s) => s.setGroupBy)
|
||||
const sortBy = useAppStore((s) => s.sortBy)
|
||||
const setSortBy = useAppStore((s) => s.setSortBy)
|
||||
const selectedSort = SORT_OPTIONS[sortBy]
|
||||
const SelectedSortIcon = selectedSort.icon
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2 pb-1.5">
|
||||
<div className="flex items-center justify-between px-2 pb-1.5 gap-1">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={groupBy}
|
||||
@@ -46,7 +18,7 @@ const GroupControls = React.memo(function GroupControls() {
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6"
|
||||
className="h-6 flex-1 justify-start"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="none"
|
||||
@@ -67,44 +39,6 @@ const GroupControls = React.memo(function GroupControls() {
|
||||
Repo
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="w-auto gap-1 px-1.5 text-muted-foreground"
|
||||
aria-label={`Sort by ${selectedSort.label}`}
|
||||
>
|
||||
<ArrowUpDown className="size-3" />
|
||||
<SelectedSortIcon className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
Sort by {selectedSort.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="min-w-0">
|
||||
{Object.entries(SORT_OPTIONS).map(([value, option]) => {
|
||||
const Icon = option.icon
|
||||
const isSelected = value === sortBy
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={value}
|
||||
onSelect={() => setSortBy(value as typeof sortBy)}
|
||||
className="pr-7"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
<span>{option.label}</span>
|
||||
{isSelected ? <Check className="ml-auto size-3.5 text-foreground" /> : null}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,40 +1,130 @@
|
||||
import React from 'react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Plus, SlidersHorizontal } from 'lucide-react'
|
||||
import { useAppStore } from '@/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import type { WorktreeCardProperty } from '../../../../shared/types'
|
||||
|
||||
const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
|
||||
{ id: 'status', label: 'Terminal status' },
|
||||
{ id: 'unread', label: 'Unread indicator' },
|
||||
{ id: 'ci', label: 'CI checks' },
|
||||
{ id: 'issue', label: 'Linked issue' },
|
||||
{ id: 'pr', label: 'Linked PR' },
|
||||
{ id: 'comment', label: 'Comment' }
|
||||
]
|
||||
|
||||
const SORT_OPTIONS = [
|
||||
{ id: 'name', label: 'Name' },
|
||||
{ id: 'recent', label: 'Recent' },
|
||||
{ id: 'repo', label: 'Repo' }
|
||||
] as const
|
||||
|
||||
const isMac = navigator.userAgent.includes('Mac')
|
||||
const newWorktreeShortcutLabel = isMac ? '⌘N' : 'Ctrl+N'
|
||||
|
||||
const SidebarHeader = React.memo(function SidebarHeader() {
|
||||
const openModal = useAppStore((s) => s.openModal)
|
||||
const repos = useAppStore((s) => s.repos)
|
||||
const canCreateWorktree = repos.length > 0
|
||||
|
||||
const worktreeCardProperties = useAppStore((s) => s.worktreeCardProperties)
|
||||
const toggleWorktreeCardProperty = useAppStore((s) => s.toggleWorktreeCardProperty)
|
||||
const sortBy = useAppStore((s) => s.sortBy)
|
||||
const setSortBy = useAppStore((s) => s.setSortBy)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 pt-3 pb-1">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground select-none">
|
||||
Worktrees
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => {
|
||||
if (!canCreateWorktree) {
|
||||
return
|
||||
}
|
||||
openModal('create-worktree')
|
||||
}}
|
||||
aria-label="Add worktree"
|
||||
disabled={!canCreateWorktree}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={6}>
|
||||
{canCreateWorktree ? 'New worktree (⌘N)' : 'Add a repo to create worktrees'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-muted-foreground"
|
||||
aria-label="View options"
|
||||
>
|
||||
<SlidersHorizontal className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
View options
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end" className="w-56 pb-2">
|
||||
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={sortBy}
|
||||
onValueChange={(v) => setSortBy(v as typeof sortBy)}
|
||||
>
|
||||
{SORT_OPTIONS.map((opt) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={opt.id}
|
||||
value={opt.id}
|
||||
// Keep the menu open so people can compare sort modes and
|
||||
// toggle card properties without reopening the same panel.
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{opt.label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Show properties</DropdownMenuLabel>
|
||||
{PROPERTY_OPTIONS.map((opt) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={opt.id}
|
||||
checked={worktreeCardProperties.includes(opt.id)}
|
||||
onCheckedChange={() => toggleWorktreeCardProperty(opt.id)}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{opt.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => {
|
||||
if (!canCreateWorktree) {
|
||||
return
|
||||
}
|
||||
openModal('create-worktree')
|
||||
}}
|
||||
aria-label="Add worktree"
|
||||
disabled={!canCreateWorktree}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={6}>
|
||||
{canCreateWorktree
|
||||
? `New worktree (${newWorktreeShortcutLabel})`
|
||||
: 'Add a repo to create worktrees'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -99,6 +99,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
const updateWorktreeMeta = useAppStore((s) => s.updateWorktreeMeta)
|
||||
const fetchPRForBranch = useAppStore((s) => s.fetchPRForBranch)
|
||||
const fetchIssue = useAppStore((s) => s.fetchIssue)
|
||||
const cardProps = useAppStore((s) => s.worktreeCardProperties)
|
||||
const handleEditIssue = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -166,18 +167,23 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
return liveTabs.length > 0 ? 'active' : 'inactive'
|
||||
}, [hasTerminals, tabs])
|
||||
|
||||
// Fetch PR data on mount. The store handles freshness checks, and
|
||||
// activity-based refresh is triggered by setActiveWorktree + visibilitychange.
|
||||
const showPR = cardProps.includes('pr')
|
||||
const showCI = cardProps.includes('ci')
|
||||
const showIssue = cardProps.includes('issue')
|
||||
|
||||
// Skip GitHub fetches when the corresponding card sections are hidden.
|
||||
// This preference is purely presentational, so background refreshes would
|
||||
// spend rate limit budget on data the user cannot see.
|
||||
useEffect(() => {
|
||||
if (repo && !worktree.isBare && prCacheKey) {
|
||||
if (repo && !worktree.isBare && prCacheKey && (showPR || showCI)) {
|
||||
fetchPRForBranch(repo.path, branch)
|
||||
}
|
||||
}, [repo, worktree.isBare, fetchPRForBranch, branch, prCacheKey])
|
||||
}, [repo, worktree.isBare, fetchPRForBranch, branch, prCacheKey, showPR, showCI])
|
||||
|
||||
// Fetch issue data on mount + background poll as safety net.
|
||||
// Primary refresh comes from setActiveWorktree + visibilitychange.
|
||||
// Same rationale for issues: once that section is hidden, polling only burns
|
||||
// GitHub calls and keeps stale-but-invisible data warm for no user benefit.
|
||||
useEffect(() => {
|
||||
if (!repo || !worktree.linkedIssue || !issueCacheKey) {
|
||||
if (!repo || !worktree.linkedIssue || !issueCacheKey || !showIssue) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -189,7 +195,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
}, 5 * 60_000) // 5 minutes
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [repo, worktree.linkedIssue, fetchIssue, issueCacheKey])
|
||||
}, [repo, worktree.linkedIssue, fetchIssue, issueCacheKey, showIssue])
|
||||
|
||||
// Stable click handler – ignore clicks that are really text selections
|
||||
const handleClick = useCallback(() => {
|
||||
@@ -244,33 +250,37 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
)}
|
||||
|
||||
{/* Status indicator on the left */}
|
||||
<div className="flex flex-col items-center justify-start pt-[2px] gap-2 shrink-0">
|
||||
<StatusIndicator status={status} />
|
||||
{(cardProps.includes('status') || cardProps.includes('unread')) && (
|
||||
<div className="flex flex-col items-center justify-start pt-[2px] gap-2 shrink-0">
|
||||
{cardProps.includes('status') && <StatusIndicator status={status} />}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleUnreadQuick}
|
||||
className={cn(
|
||||
'group/unread flex size-4 cursor-pointer items-center justify-center rounded transition-all',
|
||||
'hover:bg-accent/80 active:scale-95',
|
||||
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
||||
)}
|
||||
aria-label={worktree.isUnread ? 'Mark as read' : 'Mark as unread'}
|
||||
>
|
||||
{worktree.isUnread ? (
|
||||
<FilledBellIcon className="size-[13px] text-amber-500 drop-shadow-sm" />
|
||||
) : (
|
||||
<Bell className="size-3 text-muted-foreground/40 opacity-0 group-hover:opacity-100 group-hover/unread:opacity-100 transition-opacity" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<span>{unreadTooltip}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{cardProps.includes('unread') && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleUnreadQuick}
|
||||
className={cn(
|
||||
'group/unread flex size-4 cursor-pointer items-center justify-center rounded transition-all',
|
||||
'hover:bg-accent/80 active:scale-95',
|
||||
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
||||
)}
|
||||
aria-label={worktree.isUnread ? 'Mark as read' : 'Mark as unread'}
|
||||
>
|
||||
{worktree.isUnread ? (
|
||||
<FilledBellIcon className="size-[13px] text-amber-500 drop-shadow-sm" />
|
||||
) : (
|
||||
<Bell className="size-3 text-muted-foreground/40 opacity-0 group-hover:opacity-100 group-hover/unread:opacity-100 transition-opacity" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
<span>{unreadTooltip}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content area */}
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
||||
@@ -281,8 +291,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
</div>
|
||||
|
||||
{/* CI Checks & PR state on the right */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{pr && pr.checksStatus !== 'neutral' && (
|
||||
{cardProps.includes('ci') && pr && pr.checksStatus !== 'neutral' && (
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center opacity-80 hover:opacity-100 transition-opacity">
|
||||
@@ -301,8 +311,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
<span>CI checks {checksLabel(pr.checksStatus).toLowerCase()}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtitle row: Repo badge + Branch */}
|
||||
@@ -348,9 +358,11 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
</div>
|
||||
|
||||
{/* Meta section: Issue / PR Links / Comment */}
|
||||
{(issue || worktree.comment || pr) && (
|
||||
{((cardProps.includes('issue') && issue) ||
|
||||
(cardProps.includes('pr') && pr) ||
|
||||
(cardProps.includes('comment') && worktree.comment)) && (
|
||||
<div className="flex flex-col gap-[3px] mt-0.5">
|
||||
{issue && (
|
||||
{cardProps.includes('issue') && issue && (
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
@@ -400,7 +412,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
</HoverCard>
|
||||
)}
|
||||
|
||||
{pr && (
|
||||
{cardProps.includes('pr') && pr && (
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
@@ -462,7 +474,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
|
||||
</HoverCard>
|
||||
)}
|
||||
|
||||
{worktree.comment && (
|
||||
{cardProps.includes('comment') && worktree.comment && (
|
||||
<HoverCard openDelay={300}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div
|
||||
|
||||
@@ -309,7 +309,13 @@ const WorktreeList = React.memo(function WorktreeList() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className="flex-1 overflow-auto px-1 pt-px scrollbar-sleek scroll-smooth">
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className={cn(
|
||||
'flex-1 overflow-auto px-1 scrollbar-sleek scroll-smooth',
|
||||
groupBy === 'none' ? 'pt-2' : 'pt-px'
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full" style={{ height: `${virtualizer.getTotalSize()}px` }}>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const row = rows[vItem.index]
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Sidebar(): React.JSX.Element {
|
||||
}
|
||||
}, [repoCount, fetchAllWorktrees])
|
||||
|
||||
// ─── Resize logic ────────────────────────────────────
|
||||
// ─── Resize logic ───────────────────────────────────────────────────
|
||||
const isResizing = useRef(false)
|
||||
const startX = useRef(0)
|
||||
const startWidth = useRef(0)
|
||||
|
||||
@@ -86,7 +86,10 @@ export default function TerminalPane({
|
||||
// reorder) don't clobber previously captured scrollback.
|
||||
const existing = useAppStore.getState().terminalLayoutsByTabId[tabId]
|
||||
if (existing?.buffersByLeafId) {
|
||||
layout.buffersByLeafId = existing.buffersByLeafId
|
||||
const currentLeafIds = new Set(manager.getPanes().map((p) => paneLeafId(p.id)))
|
||||
layout.buffersByLeafId = Object.fromEntries(
|
||||
Object.entries(existing.buffersByLeafId).filter(([id]) => currentLeafIds.has(id))
|
||||
)
|
||||
}
|
||||
setTabLayout(tabId, layout)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ export function restoreScrollbackBuffers(
|
||||
}
|
||||
try {
|
||||
let buf = buffer
|
||||
// If buffer ends in alt-screen mode (agent TUI was running at
|
||||
// shutdown), exit alt-screen so the user sees a usable terminal.
|
||||
const lastOn = buf.lastIndexOf(ALT_SCREEN_ON)
|
||||
const lastOff = buf.lastIndexOf(ALT_SCREEN_OFF)
|
||||
if (lastOn > lastOff) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { AppState } from '../types'
|
||||
import type { PersistedUIState, UpdateStatus } from '../../../../shared/types'
|
||||
import type { PersistedUIState, UpdateStatus, WorktreeCardProperty } from '../../../../shared/types'
|
||||
import { DEFAULT_WORKTREE_CARD_PROPERTIES } from '../../../../shared/constants'
|
||||
|
||||
type LegacyPersistedSortBy = PersistedUIState['sortBy'] | 'smart'
|
||||
|
||||
@@ -26,6 +27,8 @@ export type UISlice = {
|
||||
setShowActiveOnly: (v: boolean) => void
|
||||
filterRepoIds: string[]
|
||||
setFilterRepoIds: (ids: string[]) => void
|
||||
worktreeCardProperties: WorktreeCardProperty[]
|
||||
toggleWorktreeCardProperty: (prop: WorktreeCardProperty) => void
|
||||
pendingRevealWorktreeId: string | null
|
||||
revealWorktreeInSidebar: (worktreeId: string) => void
|
||||
clearPendingRevealWorktreeId: () => void
|
||||
@@ -67,6 +70,17 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set) => (
|
||||
filterRepoIds: [],
|
||||
setFilterRepoIds: (ids) => set({ filterRepoIds: ids }),
|
||||
|
||||
worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES],
|
||||
toggleWorktreeCardProperty: (prop) =>
|
||||
set((s) => {
|
||||
const current = s.worktreeCardProperties || DEFAULT_WORKTREE_CARD_PROPERTIES
|
||||
const updated = current.includes(prop)
|
||||
? current.filter((p) => p !== prop)
|
||||
: [...current, prop]
|
||||
window.api.ui.set({ worktreeCardProperties: updated }).catch(console.error)
|
||||
return { worktreeCardProperties: updated }
|
||||
}),
|
||||
|
||||
pendingRevealWorktreeId: null,
|
||||
revealWorktreeInSidebar: (worktreeId) => set({ pendingRevealWorktreeId: worktreeId }),
|
||||
clearPendingRevealWorktreeId: () => set({ pendingRevealWorktreeId: null }),
|
||||
@@ -82,6 +96,7 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set) => (
|
||||
groupBy: ui.groupBy,
|
||||
sortBy,
|
||||
filterRepoIds: (ui.filterRepoIds ?? []).filter((repoId) => validRepoIds.has(repoId)),
|
||||
worktreeCardProperties: ui.worktreeCardProperties ?? [...DEFAULT_WORKTREE_CARD_PROPERTIES],
|
||||
persistedUIReady: true
|
||||
}
|
||||
}),
|
||||
|
||||
+13
-2
@@ -3,12 +3,22 @@ import type {
|
||||
PersistedState,
|
||||
PersistedUIState,
|
||||
RepoHookSettings,
|
||||
WorkspaceSessionState
|
||||
WorkspaceSessionState,
|
||||
WorktreeCardProperty
|
||||
} from './types'
|
||||
import { DEFAULT_TERMINAL_FONT_WEIGHT } from './terminal-fonts'
|
||||
|
||||
export const SCHEMA_VERSION = 1
|
||||
|
||||
export const DEFAULT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = [
|
||||
'status',
|
||||
'unread',
|
||||
'ci',
|
||||
'issue',
|
||||
'pr',
|
||||
'comment'
|
||||
]
|
||||
|
||||
export const REPO_COLORS = [
|
||||
'#737373', // neutral
|
||||
'#ef4444', // red
|
||||
@@ -77,7 +87,8 @@ export function getDefaultUIState(): PersistedUIState {
|
||||
groupBy: 'none',
|
||||
sortBy: 'name',
|
||||
filterRepoIds: [],
|
||||
uiZoomLevel: 0
|
||||
uiZoomLevel: 0,
|
||||
worktreeCardProperties: [...DEFAULT_WORKTREE_CARD_PROPERTIES]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,6 +194,8 @@ export type GlobalSettings = {
|
||||
rightSidebarOpenByDefault: boolean
|
||||
}
|
||||
|
||||
export type WorktreeCardProperty = 'status' | 'unread' | 'ci' | 'issue' | 'pr' | 'comment'
|
||||
|
||||
export type PersistedUIState = {
|
||||
lastActiveRepoId: string | null
|
||||
lastActiveWorktreeId: string | null
|
||||
@@ -203,6 +205,7 @@ export type PersistedUIState = {
|
||||
sortBy: 'name' | 'recent' | 'repo'
|
||||
filterRepoIds: string[]
|
||||
uiZoomLevel: number
|
||||
worktreeCardProperties: WorktreeCardProperty[]
|
||||
}
|
||||
|
||||
// ─── Persistence shape ──────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user