diff --git a/changes/152.added.md b/changes/152.added.md new file mode 100644 index 00000000..b10b70fe --- /dev/null +++ b/changes/152.added.md @@ -0,0 +1,6 @@ +**Every memory shows where it came from, how sure Sentient is, and whether it still holds.** Open a memory to see each +place that told Sentient, with the words it said, and jump straight to that chat message, email or note. Sentient gets +surer each time something says it again, and you can mark a memory as not right so it stays out of chats and tasks. +When a newer memory replaces an old one, the old one is kept as history instead of disappearing. Deleting a memory now +also removes what was built from it: chat summaries, the nightly journal's notes, insights about you and the lines in +USER.md and MEMORY.md. Memories learned in Telegram, Discord, WhatsApp or on a device before this update now say so. diff --git a/desktop/src/features/chat/ChatPage.tsx b/desktop/src/features/chat/ChatPage.tsx index 25436cfd..2e65382d 100644 --- a/desktop/src/features/chat/ChatPage.tsx +++ b/desktop/src/features/chat/ChatPage.tsx @@ -145,6 +145,27 @@ function ChatView({ return [...history, ...liveItems(liveSession)] }, [messages.data, liveSession]) + // ?message= (a memory's source, #152): scroll to that message once it is on screen, and mark it a moment + const focusId = new URLSearchParams(useLocation().search).get("message") + const loadedAll = !messages.isFetching + const focused = useRef(null) // done once per link, not again when the chat refetches + useEffect(() => { + if (!focusId || items.length === 0 || !loadedAll || focused.current === focusId) return + const t = window.setTimeout(() => { + focused.current = focusId + const el = document.querySelector(`[data-message-id="${CSS.escape(focusId)}"]`) + if (!el) { + // this view shows a chat's newest 500 messages: say so rather than leave the user guessing + if (!(messages.data ?? []).some((m) => m.id === focusId)) toast.info('That message is further back in this chat than Sentient shows here.') + return + } + el.scrollIntoView({ block: 'center' }) + el.classList.add('ring-2', 'ring-accent/60') + window.setTimeout(() => el.classList.remove('ring-2', 'ring-accent/60'), 2400) + }, 350) + return () => window.clearTimeout(t) + }, [focusId, items.length, loadedAll]) + const streaming = !!liveSession?.streaming const lastAssistantIndex = items.map((i) => i.kind).lastIndexOf('assistant') @@ -397,8 +418,9 @@ function Timeline({ items, streaming, loading, children }: { items: TimelineItem el.scrollTo({ top: el.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }) }, []) - // `?at=top` keeps the first message in view (deep links from search, screenshots). - const atTop = useRef(/(^|[?&])at=top\b/.test(useLocation().search)) + // `?at=top` keeps the first message in view (deep links from search, screenshots); `?message=` (a memory's source, + // #152) doesn't jump to the bottom either, so the page can scroll to that message. + const atTop = useRef(/(^|[?&])(at=top\b|message=)/.test(useLocation().search)) if (atTop.current) stick.current = false useLayoutEffect(() => { diff --git a/desktop/src/features/chat/UserMessage.tsx b/desktop/src/features/chat/UserMessage.tsx index 4c68c2a5..8dc2de9e 100644 --- a/desktop/src/features/chat/UserMessage.tsx +++ b/desktop/src/features/chat/UserMessage.tsx @@ -38,7 +38,7 @@ export function AttachmentChip({ a, className }: { a: AttachmentView; className? export const UserMessage = memo(function UserMessage({ message, onRestore }: { message: UserMessageView; onRestore?: (text: string) => void }) { const [copied, setCopied] = useState(false) return ( -
+
{message.attachments.length > 0 && (
{message.attachments.map((a) => ( diff --git a/desktop/src/features/memory/MemoryDetail.tsx b/desktop/src/features/memory/MemoryDetail.tsx index 63dcf074..b78619de 100644 --- a/desktop/src/features/memory/MemoryDetail.tsx +++ b/desktop/src/features/memory/MemoryDetail.tsx @@ -1,12 +1,13 @@ -import { IconCalendar, IconCheck, IconClock, IconHistory, IconHourglassHigh, IconPencil, IconTrash, IconX } from '@tabler/icons-react' +import { IconCalendar, IconCheck, IconClock, IconGauge, IconHistory, IconHourglassHigh, IconMoodConfuzed, IconPencil, IconReplace, IconRoute, IconTrash, IconX } from '@tabler/icons-react' import { useEffect, useState } from 'react' import { Alert, Button, ConfirmDialog, Select, Sheet, Textarea } from '@/components/ui' import { useMemoryScopes } from '@/hooks/memory' import { useHotkey } from '@/hooks/useHotkey' import { formatDateTime } from '@/lib/utils' -import { ExpiryBadge, ScopeIcon, scopeLabel, SourceChip, TopicPill } from './bits' +import { ExpiryBadge, ScopeIcon, scopeLabel, TopicPill } from './bits' import { useNow, useOptimisticMemoryActions } from './hooks' import { countdown, expiresInMs, recentRelative, sourceMeta, type MemoryWithHistory } from './meta' +import { Confidence, SourceLinks } from './SourceLinks' export function MemoryDetail({ memory, @@ -19,7 +20,7 @@ export function MemoryDetail({ onOpenChange: (open: boolean) => void onForgetSource: (source: string) => void }) { - const { update, remove, move } = useOptimisticMemoryActions() + const { update, remove, move, setState } = useOptimisticMemoryActions() const scopes = useMemoryScopes() const [editing, setEditing] = useState(false) const [draft, setDraft] = useState('') @@ -91,6 +92,24 @@ export function MemoryDetail({
+ {memory.state === 'disputed' && ( + } title="You said this isn't right"> + Sentient keeps it here but leaves it out of chats and tasks. Edit it, forget it, or{' '} + + . + + )} + {memory.state === 'superseded' && ( + } title="Replaced by a newer memory"> + {memory.superseded_by_content ? `Now: "${memory.superseded_by_content}". ` : ''}Sentient keeps this one as history and doesn't use it.{' '} + + + )} + {memory.previous_content && memory.previous_content !== memory.content && (
@@ -100,15 +119,25 @@ export function MemoryDetail({
)} +
+

+ Where it came from +

+ +
+
- } label="Source"> -
- - -
-
{src.description}
+ )}
} label="Used in">