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:
Serhii Dziupin
2026-06-26 19:36:47 +03:00
committed by GitHub
co-authored by Serhii Dziupin
parent e7b952cbcb
commit 3e3cd82a47
19 changed files with 377 additions and 45 deletions
+77
View File
@@ -0,0 +1,77 @@
import { useSyncExternalStore } from 'react';
/**
* Tracks whether the Shift key is currently held, shared across all consumers
* via a single set of window listeners.
*
* Using one module-level listener set (instead of per-component listeners)
* keeps this cheap even when many rows subscribe, and `useSyncExternalStore`
* lets unrelated subtrees stay isolated — only components that actually call
* this hook re-render when the Shift state flips.
*/
let shiftHeld = false;
const listeners = new Set<() => void>();
let initialized = false;
function emit(): void {
for (const listener of listeners) {
listener();
}
}
function setShiftHeld(next: boolean): void {
if (shiftHeld === next) {
return;
}
shiftHeld = next;
emit();
}
function handleKeyDown(event: KeyboardEvent): void {
if (event.key === 'Shift') {
setShiftHeld(true);
}
}
function handleKeyUp(event: KeyboardEvent): void {
if (event.key === 'Shift') {
setShiftHeld(false);
}
}
// The window can lose focus while Shift is held (e.g. alt-tab), and the
// matching keyup never arrives — reset so the UI doesn't get stuck in the
// "delete" affordance.
function handleReset(): void {
setShiftHeld(false);
}
function ensureListeners(): void {
if (initialized || typeof window === 'undefined') {
return;
}
initialized = true;
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
window.addEventListener('blur', handleReset);
}
function subscribe(onStoreChange: () => void): () => void {
ensureListeners();
listeners.add(onStoreChange);
return () => {
listeners.delete(onStoreChange);
};
}
function getSnapshot(): boolean {
return shiftHeld;
}
function getServerSnapshot(): boolean {
return false;
}
export function useShiftKeyHeld(): boolean {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}