feat: inherit model-picker reorder/accordion persistence and shift-delete from otto-ui (#1833)
* feat(model-picker): drag-to-reorder providers and persist accordion state Ports two model/provider picker QoL features from otto-ui: - Persist collapsed state of picker sections (favorites, recent, each provider) via a new persisted zustand store so collapse survives remounts and reloads, shared across every picker surface. - Desktop drag-to-reorder of provider sections (whole header as the mouse activator, 8px threshold so a plain click still toggles collapse), with the order persisted in useUIStore.providerOrder and applied across ModelControls, ModelMultiSelect and ModelSelector. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * feat(sessions): shift-click quick action hard-deletes thread without prompt Ports the shift-quick-delete feature from otto-ui. The sidebar quick action (normally archive) becomes a no-prompt hard delete while Shift is held: the icon switches to a trash bin, the affordance turns destructive, and the click bypasses the confirmation dialog via a new skipConfirm source flag. A shared useShiftKeyHeld hook (single window listener set, useSyncExternalStore) keeps only the small action button re-rendering on Shift state changes, and resets on window blur so the affordance can't get stuck after alt-tab. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
Serhii Dziupin
parent
e7b952cbcb
commit
3e3cd82a47
@@ -33,6 +33,7 @@ import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
|
||||
import { useSessionUnseenCount } from '@/sync/notification-store';
|
||||
import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useShiftKeyHeld } from '@/hooks/useShiftKeyHeld';
|
||||
import { getRuntimeBearerTokenSync } from '@/lib/runtime-auth';
|
||||
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
|
||||
import { parseMultiRunSessionTitle } from '@/lib/multirun/title';
|
||||
@@ -82,7 +83,7 @@ type Props = {
|
||||
addSessionToFolder: (scopeKey: string, folderId: string, sessionId: string) => void;
|
||||
createFolderAndStartRename: (scopeKey: string, parentId?: string | null) => { id: string } | null;
|
||||
openContextPanelTab: (directory: string, options: { mode: 'chat'; dedupeKey: string; label: string; readOnly?: boolean }) => void;
|
||||
handleDeleteSession: (session: Session, source?: { archivedBucket?: boolean; hardDelete?: boolean }) => void;
|
||||
handleDeleteSession: (session: Session, source?: { archivedBucket?: boolean; hardDelete?: boolean; skipConfirm?: boolean }) => void;
|
||||
mobileVariant: boolean;
|
||||
alwaysShowActions: boolean;
|
||||
renderSessionNode: (
|
||||
@@ -139,6 +140,68 @@ type Props = {
|
||||
liveSessionById: Map<string, Session>;
|
||||
};
|
||||
|
||||
type QuickSessionActionProps = {
|
||||
archiveLabel: string;
|
||||
deleteLabel: string;
|
||||
buttonSizeClass: string;
|
||||
iconSizeClass: string;
|
||||
onPointerDown: (event: React.PointerEvent<HTMLButtonElement>) => void;
|
||||
onMouseDown: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onArchive: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDelete: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
};
|
||||
|
||||
// Extracted so only this small button re-renders when Shift is pressed/released,
|
||||
// instead of every mounted session row.
|
||||
const QuickSessionAction = React.memo(function QuickSessionAction({
|
||||
archiveLabel,
|
||||
deleteLabel,
|
||||
buttonSizeClass,
|
||||
iconSizeClass,
|
||||
onPointerDown,
|
||||
onMouseDown,
|
||||
onArchive,
|
||||
onDelete,
|
||||
}: QuickSessionActionProps): React.ReactNode {
|
||||
const shiftHeld = useShiftKeyHeld();
|
||||
const label = shiftHeld ? deleteLabel : archiveLabel;
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (shiftHeld || event.shiftKey) {
|
||||
onDelete(event);
|
||||
return;
|
||||
}
|
||||
onArchive(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
shiftHeld
|
||||
? 'text-destructive hover:text-destructive'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
buttonSizeClass,
|
||||
)}
|
||||
aria-label={label}
|
||||
onPointerDown={onPointerDown}
|
||||
onMouseDown={onMouseDown}
|
||||
onClick={handleClick}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Icon name={shiftHeld ? 'delete-bin' : 'archive'} className={iconSizeClass} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={8}>
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
@@ -634,6 +697,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
handleDeleteSession(session, { archivedBucket });
|
||||
};
|
||||
|
||||
const handleQuickDeleteClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenSidebarMenuKey(null);
|
||||
handleDeleteSession(session, { archivedBucket, hardDelete: true, skipConfirm: true });
|
||||
};
|
||||
|
||||
const handleOpenInEditorPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1046,27 +1116,16 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
: cn('opacity-0', revealOnHoverClass),
|
||||
)}>
|
||||
{showQuickArchiveAction ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
|
||||
isMinimalMode && !alwaysShowActions ? 'h-4 w-4' : 'h-6 w-6',
|
||||
)}
|
||||
aria-label={t('sessions.sidebar.bulkActions.archive')}
|
||||
onPointerDown={handleQuickArchivePointerDown}
|
||||
onMouseDown={handleQuickArchiveMouseDown}
|
||||
onClick={handleQuickArchiveClick}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Icon name="archive" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" sideOffset={8}>
|
||||
{t('sessions.sidebar.bulkActions.archive')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<QuickSessionAction
|
||||
archiveLabel={t('sessions.sidebar.bulkActions.archive')}
|
||||
deleteLabel={t('sessions.sidebar.bulkActions.delete')}
|
||||
buttonSizeClass={isMinimalMode && !alwaysShowActions ? 'h-4 w-4' : 'h-6 w-6'}
|
||||
iconSizeClass={isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5'}
|
||||
onPointerDown={handleQuickArchivePointerDown}
|
||||
onMouseDown={handleQuickArchiveMouseDown}
|
||||
onArchive={handleQuickArchiveClick}
|
||||
onDelete={handleQuickDeleteClick}
|
||||
/>
|
||||
) : null}
|
||||
{showOpenInEditorAction ? (
|
||||
<Tooltip>
|
||||
|
||||
@@ -15,6 +15,8 @@ type DeleteSessionConfirmSetter = React.Dispatch<React.SetStateAction<{
|
||||
type DeleteSessionSource = {
|
||||
archivedBucket?: boolean;
|
||||
hardDelete?: boolean;
|
||||
/** Bypass the confirmation dialog and delete/archive immediately. */
|
||||
skipConfirm?: boolean;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
@@ -253,7 +255,7 @@ export const useSessionActions = (args: Args) => {
|
||||
collectDescendants(session.id),
|
||||
shouldHardDelete,
|
||||
).map((s) => s.id);
|
||||
if (!args.showDeletionDialog) {
|
||||
if (!args.showDeletionDialog || source?.skipConfirm === true) {
|
||||
void executeDeleteSession(session, source, { descendantIds: effectiveDescendantIds });
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user