hermes-agent/apps/desktop/src/store/tool-dismiss.ts
Brooklyn Nicholson b16e22b8f2 fix(desktop): persist tool-row dismissal across virtualization; keep caret hittable
Salvage of #45240. The dismiss-settled-tool-rows affordance was correct in
intent but had two issues against current main:

- The thread is virtualized, so a row's component unmounts/remounts as it
  scrolls. Component-local `useState` dismissal was forgotten on remount and
  the row popped back. Move dismissal into a session-scoped nanostore keyed by
  the stable disclosure id (mirrors $toolDisclosureOpen), so a dismissed row
  stays gone while scrolling but a reload restores real history instead of
  permanently rewriting it.
- The dismiss button lived in DisclosureRow's absolute `trailing` slot — the
  exact "opacity-0-but-clickable control fights the caret" pattern the trailing
  comment warns against. Add an in-flow `action` slot that lays out at the far
  right so an interactive control never overlaps the caret's hit-target,
  regardless of title length, and move the dismiss button into it.

Adds a remount regression test alongside the existing dismissal coverage.
2026-06-12 17:34:48 -05:00

45 lines
1.6 KiB
TypeScript

import { atom, computed, type ReadableAtom } from 'nanostores'
type DismissedToolRows = Record<string, true>
// Tool rows the user has locally hidden via a row's dismiss control. This is a
// *view-only* hide: the underlying tool call still lives in the stored chat
// history, but once a turn has settled the user can clear a completed/failed
// row out of the way so it stops sitting at the tail of the conversation.
//
// Kept in module memory (not localStorage, unlike $toolDisclosureStates) on
// purpose: the thread is virtualized, so a dismissed row's component unmounts
// and remounts as it scrolls — component-local state would forget the dismissal
// and the row would pop back. Storing it here survives those remounts for the
// life of the app session, while a reload restores every row in place rather
// than permanently rewriting history from a stray click.
export const $dismissedToolRows = atom<DismissedToolRows>({})
const dismissedCache = new Map<string, ReadableAtom<boolean>>()
export function $toolRowDismissed(id: string): ReadableAtom<boolean> {
let cached = dismissedCache.get(id)
if (!cached) {
cached = computed($dismissedToolRows, rows => Boolean(rows[id]))
dismissedCache.set(id, cached)
}
return cached
}
export function dismissToolRow(id: string) {
if (!id || $dismissedToolRows.get()[id]) {
return
}
$dismissedToolRows.set({ ...$dismissedToolRows.get(), [id]: true })
}
export function clearDismissedToolRows() {
if (Object.keys($dismissedToolRows.get()).length === 0) {
return
}
$dismissedToolRows.set({})
}