fix(desktop): ship the sidebar grouped by date in every scope
The all-profiles scope defaulted to grouping by profile, so "Reset to defaults" handed back a grouping the user never picked. Both scopes now ship by date, and a reset clears the scope you are not looking at too — otherwise flipping the rail restored the customization the reset was supposed to undo. Hovering a row's PR chip also holds the kebab back now: the chip is a link, and the button that covers the end of the trailing slot was taking the click.
This commit is contained in:
parent
8de786c7a7
commit
7e1f4f6f36
|
|
@ -37,6 +37,10 @@ export function PrTag({ className, pr }: { className?: string; pr: HermesBranchP
|
|||
style.className,
|
||||
className
|
||||
)}
|
||||
// Marks the chip as a live link for the row's hover rule: while the
|
||||
// pointer is on it, the row keeps its metadata and holds the kebab back
|
||||
// (see session-row) so the click can actually land.
|
||||
data-pr-link
|
||||
onClick={event => {
|
||||
// The row underneath opens the session on click and pins on
|
||||
// shift-click; the chip is its own target and keeps the press.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ interface SidebarSessionRowProps extends React.ComponentProps<'div'> {
|
|||
|
||||
const AGE_KEY = { day: 'ageDay', hour: 'ageHour', minute: 'ageMin' } as const
|
||||
|
||||
// The last thing in the trailing slot hands its place to the ⋯ button on hover,
|
||||
// and is never narrower than the button that has to cover it. A PR chip is the
|
||||
// exception while the pointer is on it: it's a link, and the kebab sits
|
||||
// absolute over this space, so it has to stop taking clicks too, not just fade.
|
||||
const TAIL_HIDES = 'min-w-5 transition-opacity group-hover:opacity-0 group-has-[[data-pr-link]:hover]:opacity-100'
|
||||
const KEBAB_YIELDS = 'group-has-[[data-pr-link]:hover]:pointer-events-none group-has-[[data-pr-link]:hover]:opacity-0'
|
||||
|
||||
function formatAge(seconds: number, r: Translations['sidebar']['row']): string {
|
||||
const { unit, value } = coarseElapsed(Date.now() - seconds * 1000)
|
||||
|
||||
|
|
@ -148,7 +155,7 @@ function SidebarSessionRowImpl({
|
|||
<span className="pointer-events-none whitespace-nowrap text-[0.625rem] leading-none text-(--ui-text-tertiary)">
|
||||
{head}
|
||||
{/* The figures own their tail: the separator goes with it. */}
|
||||
<span className="inline-block min-w-5 text-right transition-opacity group-hover:opacity-0">
|
||||
<span className={cn('inline-block text-right', TAIL_HIDES)}>
|
||||
{head && ' · '}
|
||||
{figures.at(-1)}
|
||||
</span>
|
||||
|
|
@ -200,12 +207,9 @@ function SidebarSessionRowImpl({
|
|||
<div className="relative z-2 flex items-center justify-end gap-1" data-row-actions>
|
||||
{trailing.map(({ key, node }, index) => (
|
||||
<span
|
||||
className={cn(
|
||||
// Never narrower than the kebab that has to cover it.
|
||||
chipEndsSlot &&
|
||||
index === trailing.length - 1 &&
|
||||
'inline-flex min-w-5 justify-end transition-opacity group-hover:opacity-0'
|
||||
)}
|
||||
className={
|
||||
chipEndsSlot && index === trailing.length - 1 ? cn('inline-flex justify-end', TAIL_HIDES) : undefined
|
||||
}
|
||||
key={key}
|
||||
>
|
||||
{node}
|
||||
|
|
@ -225,7 +229,8 @@ function SidebarSessionRowImpl({
|
|||
aria-label={r.sessionActions}
|
||||
className={cn(
|
||||
'size-5 rounded-[4px] bg-transparent text-transparent transition-colors duration-100 hover:bg-(--ui-control-active-background) hover:text-foreground focus-visible:bg-(--ui-control-active-background) focus-visible:text-foreground focus-visible:ring-0 data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground group-hover:text-(--ui-text-tertiary) [&_svg]:size-3.5!',
|
||||
trailing.length > 0 && 'absolute right-0'
|
||||
trailing.length > 0 && 'absolute right-0',
|
||||
pr && KEBAB_YIELDS
|
||||
)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -48,16 +48,27 @@ describe('the sidebar as it ships', () => {
|
|||
expect($sidebarViewCustomized.get()).toBe(false)
|
||||
})
|
||||
|
||||
it('resets to the all-profiles default while that scope is on', () => {
|
||||
it('ships by date in the all-profiles scope too, and resets back to it', () => {
|
||||
$showAllProfiles.set(true)
|
||||
setSidebarGrouping('status')
|
||||
setSidebarGrouping('profile')
|
||||
|
||||
resetSidebarView()
|
||||
|
||||
expect($sidebarGrouping.get()).toBe('profile')
|
||||
expect($sidebarGrouping.get()).toBe('date')
|
||||
expect($sidebarViewCustomized.get()).toBe(false)
|
||||
})
|
||||
|
||||
it('resets the scope the user is not looking at, so flipping the rail cannot restore it', () => {
|
||||
setSidebarGrouping('status')
|
||||
$showAllProfiles.set(true)
|
||||
setSidebarGrouping('profile')
|
||||
|
||||
resetSidebarView()
|
||||
$showAllProfiles.set(false)
|
||||
|
||||
expect($sidebarGrouping.get()).toBe('date')
|
||||
})
|
||||
|
||||
it('turns all-profiles on when the user groups by profile, since that is the ask', () => {
|
||||
setSidebarGrouping('profile')
|
||||
|
||||
|
|
|
|||
|
|
@ -243,24 +243,20 @@ const $sidebarFlatGrouping = persistentAtom<SidebarGrouping>(
|
|||
oneOf(['date', 'status'], 'date')
|
||||
)
|
||||
|
||||
// All-profiles keeps its own grouping. The two scopes want different answers —
|
||||
// one profile's chats read best by day, everyone's read best by owner — and a
|
||||
// shared atom would either drag `profile` into a scope where it means nothing
|
||||
// or reset the choice every time the user flips the rail.
|
||||
// All-profiles keeps its own grouping: `profile` only means anything there, and
|
||||
// a shared atom would either drag it into a scope where it means nothing or
|
||||
// reset the choice every time the user flips the rail. Both scopes still ship
|
||||
// by day — grouping by owner is something you go and pick.
|
||||
const $sidebarAllProfilesGrouping = persistentAtom<SidebarGrouping>(
|
||||
SIDEBAR_ALL_PROFILES_GROUPING_STORAGE_KEY,
|
||||
'profile',
|
||||
oneOf(['date', 'profile', 'status'], 'profile')
|
||||
)
|
||||
|
||||
/** What the active scope groups by before the user touches anything. */
|
||||
export const $sidebarDefaultGrouping: ReadableAtom<SidebarGrouping> = computed([$showAllProfiles], showAll =>
|
||||
showAll ? 'profile' : 'date'
|
||||
'date',
|
||||
oneOf(['date', 'profile', 'status'], 'date')
|
||||
)
|
||||
|
||||
// The sidebar as it ships. Declared once so the atoms below, "Reset to
|
||||
// defaults" and the "has this view been customized?" check can't drift apart —
|
||||
// they used to inline the same literals in three places.
|
||||
const SIDEBAR_DEFAULT_GROUPING: SidebarGrouping = 'date'
|
||||
const SIDEBAR_DEFAULT_ORDERING: SidebarOrdering = 'updated'
|
||||
const SIDEBAR_DEFAULT_ROW_META: SidebarRowMeta[] = ['updated']
|
||||
|
||||
|
|
@ -339,9 +335,9 @@ export const $sidebarFiltersActive: ReadableAtom<boolean> = computed(
|
|||
* offering. Broader than `$sidebarFiltersActive`, which only knows about what
|
||||
* hides rows, not about how they're grouped, sorted or labelled. */
|
||||
export const $sidebarViewCustomized: ReadableAtom<boolean> = computed(
|
||||
[$sidebarGrouping, $sidebarDefaultGrouping, $sidebarOrdering, $sidebarRowMeta, $sidebarFiltersActive],
|
||||
(grouping, defaultGrouping, ordering, rowMeta, filtersActive) =>
|
||||
grouping !== defaultGrouping ||
|
||||
[$sidebarGrouping, $sidebarOrdering, $sidebarRowMeta, $sidebarFiltersActive],
|
||||
(grouping, ordering, rowMeta, filtersActive) =>
|
||||
grouping !== SIDEBAR_DEFAULT_GROUPING ||
|
||||
ordering !== SIDEBAR_DEFAULT_ORDERING ||
|
||||
!sameRowMeta(rowMeta, SIDEBAR_DEFAULT_ROW_META) ||
|
||||
filtersActive
|
||||
|
|
@ -619,7 +615,11 @@ function clearSidebarFilters() {
|
|||
/** Every knob the filter menu owns, back to the sidebar as it ships. Ordering
|
||||
* goes through its setter so a hand-dragged sequence is dropped along with it. */
|
||||
export function resetSidebarView() {
|
||||
setSidebarGrouping($sidebarDefaultGrouping.get())
|
||||
setSidebarGrouping(SIDEBAR_DEFAULT_GROUPING)
|
||||
// Both scopes, not just the one on screen: each keeps its own grouping, so a
|
||||
// reset that left the other customized would hand it back on the next flip.
|
||||
$sidebarFlatGrouping.set(SIDEBAR_DEFAULT_GROUPING)
|
||||
$sidebarAllProfilesGrouping.set(SIDEBAR_DEFAULT_GROUPING)
|
||||
setSidebarOrdering(SIDEBAR_DEFAULT_ORDERING)
|
||||
$sidebarRowMeta.set(SIDEBAR_DEFAULT_ROW_META)
|
||||
clearSidebarFilters()
|
||||
|
|
|
|||
|
|
@ -11929,7 +11929,7 @@ def _project_tree_row(r: dict) -> dict:
|
|||
"input_tokens": r.get("input_tokens") or 0,
|
||||
"output_tokens": r.get("output_tokens") or 0,
|
||||
# Cost is one of the fields SidebarSessionRow renders, so a lane row has
|
||||
# to carry it too — without it, switching Show → cost on filled every
|
||||
# to carry it too — without it, switching Show → cost filled in every
|
||||
# figure in Recents and left the same sessions blank under a project.
|
||||
"actual_cost_usd": r.get("actual_cost_usd"),
|
||||
"estimated_cost_usd": r.get("estimated_cost_usd"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue