refactor(desktop): drop per-session icons, read-only cross-profile reads
The per-session icon picker added more noise than value — rip it out end to end (sessions.icon column, set_session_icon, the PATCH field, the picker UI, and the SessionInfo.icon type). The cross-profile session aggregator now opens each profile's state.db read-only (mode=ro, no schema init), so listing other profiles on every sidebar refresh never DDLs or takes a write lock on their live DBs. The single-profile hot path stays on par with /api/sessions.
This commit is contained in:
parent
48d8d80771
commit
cf9dc366dd
7 changed files with 40 additions and 199 deletions
|
|
@ -15,27 +15,17 @@ import {
|
|||
} from '@/components/ui/dialog'
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { renameSession, setSessionIcon } from '@/hermes'
|
||||
import { renameSession } from '@/hermes'
|
||||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { exportSession } from '@/lib/session-export'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { setSessions } from '@/store/session'
|
||||
|
||||
// Curated glyphs for the per-session icon picker. Kept short so sessions from
|
||||
// different profiles stay visually distinguishable at a glance.
|
||||
const SESSION_ICON_CHOICES = [
|
||||
'🦊', '🐙', '🦉', '🐝', '🐢', '🦄', '🚀', '⚡',
|
||||
'🔥', '🌙', '⭐', '🎯', '🧪', '🔭', '🛠️', '🔒',
|
||||
'💼', '📦', '🎨', '🧠'
|
||||
] as const
|
||||
|
||||
interface SessionActions {
|
||||
sessionId: string
|
||||
title: string
|
||||
pinned?: boolean
|
||||
profile?: string
|
||||
icon?: null | string
|
||||
onPin?: () => void
|
||||
onArchive?: () => void
|
||||
onDelete?: () => void
|
||||
|
|
@ -52,18 +42,8 @@ interface ItemSpec {
|
|||
variant?: 'destructive'
|
||||
}
|
||||
|
||||
function useSessionActions({
|
||||
sessionId,
|
||||
title,
|
||||
pinned = false,
|
||||
profile,
|
||||
icon,
|
||||
onPin,
|
||||
onArchive,
|
||||
onDelete
|
||||
}: SessionActions) {
|
||||
function useSessionActions({ sessionId, title, pinned = false, profile, onPin, onArchive, onDelete }: SessionActions) {
|
||||
const [renameOpen, setRenameOpen] = useState(false)
|
||||
const [iconOpen, setIconOpen] = useState(false)
|
||||
|
||||
const items: ItemSpec[] = [
|
||||
{
|
||||
|
|
@ -103,15 +83,6 @@ function useSessionActions({
|
|||
setRenameOpen(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
disabled: !sessionId,
|
||||
icon: 'symbol-color',
|
||||
label: 'Icon',
|
||||
onSelect: () => {
|
||||
triggerHaptic('selection')
|
||||
setIconOpen(true)
|
||||
}
|
||||
},
|
||||
{
|
||||
disabled: !onArchive,
|
||||
icon: 'archive',
|
||||
|
|
@ -143,23 +114,13 @@ function useSessionActions({
|
|||
))
|
||||
|
||||
const renameDialog = (
|
||||
<>
|
||||
<RenameSessionDialog
|
||||
currentTitle={title}
|
||||
onOpenChange={setRenameOpen}
|
||||
open={renameOpen}
|
||||
profile={profile}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
<IconPickerDialog
|
||||
currentIcon={icon ?? null}
|
||||
onOpenChange={setIconOpen}
|
||||
open={iconOpen}
|
||||
profile={profile}
|
||||
sessionId={sessionId}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
<RenameSessionDialog
|
||||
currentTitle={title}
|
||||
onOpenChange={setRenameOpen}
|
||||
open={renameOpen}
|
||||
profile={profile}
|
||||
sessionId={sessionId}
|
||||
/>
|
||||
)
|
||||
|
||||
return { renameDialog, renderItems }
|
||||
|
|
@ -294,67 +255,3 @@ function RenameSessionDialog({ open, onOpenChange, sessionId, currentTitle, prof
|
|||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
interface IconPickerDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
sessionId: string
|
||||
title: string
|
||||
currentIcon: null | string
|
||||
profile?: string
|
||||
}
|
||||
|
||||
function IconPickerDialog({ open, onOpenChange, sessionId, title, currentIcon, profile }: IconPickerDialogProps) {
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const apply = async (icon: string) => {
|
||||
if (!sessionId || submitting) {
|
||||
return
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
const result = await setSessionIcon(sessionId, icon, profile)
|
||||
const finalIcon = result.icon ?? null
|
||||
setSessions(prev => prev.map(s => (s.id === sessionId ? { ...s, icon: finalIcon } : s)))
|
||||
onOpenChange(false)
|
||||
} catch (err) {
|
||||
notifyError(err, 'Could not set icon')
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={onOpenChange} open={open}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Session icon</DialogTitle>
|
||||
<DialogDescription>Pick a glyph for “{title || 'this session'}” so it stands out in the list.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid grid-cols-8 gap-1.5">
|
||||
{SESSION_ICON_CHOICES.map(glyph => (
|
||||
<button
|
||||
className={cn(
|
||||
'grid aspect-square place-items-center rounded-md border border-transparent text-lg transition-colors hover:bg-(--ui-control-hover-background)',
|
||||
currentIcon === glyph && 'border-(--ui-stroke-tertiary) bg-(--ui-control-active-background)'
|
||||
)}
|
||||
disabled={submitting}
|
||||
key={glyph}
|
||||
onClick={() => void apply(glyph)}
|
||||
type="button"
|
||||
>
|
||||
{glyph}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button disabled={submitting || !currentIcon} onClick={() => void apply('')} type="button" variant="ghost">
|
||||
Clear icon
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ export function SidebarSessionRow({
|
|||
|
||||
return (
|
||||
<SessionContextMenu
|
||||
icon={session.icon}
|
||||
onArchive={onArchive}
|
||||
onDelete={onDelete}
|
||||
onPin={onPin}
|
||||
|
|
@ -159,11 +158,6 @@ export function SidebarSessionRow({
|
|||
<SidebarRowDot isWorking={isWorking} needsInput={needsInput} />
|
||||
</span>
|
||||
)}
|
||||
{session.icon ? (
|
||||
<span aria-hidden="true" className="shrink-0 text-xs leading-none">
|
||||
{session.icon}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="min-w-0 flex-1 truncate text-[0.8125rem] font-normal text-(--ui-text-secondary) group-hover:text-foreground group-data-[working=true]:text-foreground/90">
|
||||
{title}
|
||||
</span>
|
||||
|
|
@ -175,7 +169,6 @@ export function SidebarSessionRow({
|
|||
</span>
|
||||
)}
|
||||
<SessionActionsMenu
|
||||
icon={session.icon}
|
||||
onArchive={onArchive}
|
||||
onDelete={onDelete}
|
||||
onPin={onPin}
|
||||
|
|
|
|||
|
|
@ -209,20 +209,6 @@ export function renameSession(
|
|||
})
|
||||
}
|
||||
|
||||
// Set ("" clears) a per-session icon glyph. `profile` targets another profile's
|
||||
// session (the backend opens its state.db). Omit for the current profile.
|
||||
export function setSessionIcon(
|
||||
id: string,
|
||||
icon: string,
|
||||
profile?: string | null
|
||||
): Promise<{ ok: boolean; icon: string | null }> {
|
||||
return window.hermesDesktop.api<{ ok: boolean; icon: string | null }>({
|
||||
path: `/api/sessions/${encodeURIComponent(id)}`,
|
||||
method: 'PATCH',
|
||||
body: { icon, ...(profile ? { profile } : {}) }
|
||||
})
|
||||
}
|
||||
|
||||
export function getGlobalModelInfo(): Promise<ModelInfoResponse> {
|
||||
return window.hermesDesktop.api<ModelInfoResponse>({
|
||||
...profileScoped(),
|
||||
|
|
|
|||
|
|
@ -291,9 +291,6 @@ export interface SessionInfo {
|
|||
profile?: string
|
||||
/** True when {@link profile} is the default profile. */
|
||||
is_default_profile?: boolean
|
||||
/** Optional per-session glyph the user picked so sessions from different
|
||||
* profiles are visually distinguishable in the unified list. */
|
||||
icon?: null | string
|
||||
}
|
||||
|
||||
export interface SessionMessage {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue