Skip to content

Commit 5469a92

Browse files
Merge branch 'main' into test/update-probe-same
2 parents d2abe6e + fa5720f commit 5469a92

24 files changed

Lines changed: 629 additions & 110 deletions

‎changes/435.fixed.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**A request waiting for your approval no longer reads as if it already happened.** A click waiting for your
2+
answer showed a line like "Clicked using browser_click" under it. Now the card says only what Sentient would do,
3+
in plain words.

‎changes/456.fixed.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**Deleting a memory no longer removes what Sentient knows from a similar one.** Before, deleting "drives a red Honda City" could also take the MEMORY.md line, journal lines and user model evidence of "sold her red Honda City and now rides a blue Ather". Now only the deleted memory's own lines and evidence go.

‎changes/458.security.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**An older chat never shows a newer screen capture or file.** A shared window, selected text or file could get the
2+
name of one that was deleted, so an older chat showed the new, unrelated picture. Every capture now gets its own name,
3+
and a chat whose attachment is gone says "No longer available".

‎desktop/src/features/chat/ApprovalCard.tsx‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import type { ApprovalDecision } from '@/lib/types'
2929
import { cn, humanize, truncate } from '@/lib/utils'
3030
import { ActionPreview } from './ActionPreview'
3131
import { hostOf, refLabel } from './cards/bits'
32-
import { toolMeta } from './toolMeta'
33-
import { approvalCardReason, resolveWording, type RiskLabel } from './approvalRisk'
32+
import { askingLabel } from './toolMeta'
33+
import { approvalCardReason, resolveWording, waitingSmallPrint, type RiskLabel } from './approvalRisk'
3434

3535
const STATUS_TEXT: Partial<Record<ApprovalView['status'], string>> = {
3636
allow: 'You allowed this once',
@@ -152,7 +152,6 @@ export function ApprovalCard({
152152
onRespond?: (approvalId: string, decision: ApprovalDecision) => void
153153
}) {
154154
const risk = effectiveRisk(approval)
155-
const meta = toolMeta(approval.name)
156155

157156
if (approval.status !== 'pending') {
158157
const allowed = approval.status !== 'deny' && approval.status !== 'expired'
@@ -193,11 +192,7 @@ export function ApprovalCard({
193192
(command && 'It runs on this computer with your permissions, in this folder:') ||
194193
purpose ||
195194
reason ||
196-
(meta.done ? (
197-
<>
198-
{meta.done} using <span className="font-mono text-xs">{approval.name}</span>
199-
</>
200-
) : null)
195+
waitingSmallPrint(action, askingLabel(approval.name))
201196

202197
return (
203198
<motion.div

‎desktop/src/features/chat/UserMessage.tsx‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { IconCheck, IconCopy, IconFile, IconPhoto, IconPlayerStop } from '@tabler/icons-react'
1+
import { IconCheck, IconCopy, IconFile, IconFileOff, IconPhoto, IconPhotoOff, IconPlayerStop } from '@tabler/icons-react'
22
import { memo, useState } from 'react'
33
import { Button, IconButton } from '@/components/ui'
4+
import { useFileExists } from '@/hooks/core'
45
import { api } from '@/lib/api'
56
import type { AttachmentView, UserMessageView } from '@/lib/chatFold'
67
import { cn, copyText, formatBytes } from '@/lib/utils'
@@ -15,10 +16,29 @@ export function AttachmentChip({ a, className }: { a: AttachmentView; className?
1516
const image = isImageAttachment(a)
1617
const src = a.previewUrl ?? (image ? api.files.contentUrl(a.name) : undefined)
1718
const base = a.name.split('/').pop() ?? a.name
18-
if (image && src) {
19+
// an attachment from the chat's history can be gone (deleted, or it went with an incognito chat): say so (#458)
20+
const [failed, setFailed] = useState(false)
21+
const exists = useFileExists(a.name, !!a.saved && (!image || failed))
22+
if (exists.data === false) {
23+
return (
24+
<div
25+
className={cn('flex h-11 max-w-60 items-center gap-2.5 rounded-xl border border-dashed border-border px-3', className)}
26+
title={base}
27+
>
28+
<div className="flex size-7 shrink-0 items-center justify-center rounded-md bg-sunken text-fg-subtle">
29+
{image ? <IconPhotoOff size={15} /> : <IconFileOff size={15} />}
30+
</div>
31+
<div className="min-w-0">
32+
<div className="truncate text-xs font-medium text-fg-muted">{base}</div>
33+
<div className="text-2xs text-fg-subtle">No longer available</div>
34+
</div>
35+
</div>
36+
)
37+
}
38+
if (image && src && !failed) {
1939
return (
2040
<div className={cn('overflow-hidden rounded-xl border border-border bg-sunken', className)} title={base}>
21-
<img src={src} alt={base} className="h-28 max-w-56 object-cover" />
41+
<img src={src} alt={base} className="h-28 max-w-56 object-cover" onError={() => setFailed(true)} />
2242
</div>
2343
)
2444
}

‎desktop/src/features/chat/approvalRisk.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,13 @@ export function approvalCardReason(reason?: string | null, badgeLabel?: string |
313313
}
314314
return trimmed
315315
}
316+
317+
/**
318+
* The small print of a waiting approval card that has no reason of its own. Nothing has happened yet, so never
319+
* past tense and never the tool's developer name: nothing when the action line above already says what will happen
320+
* ("Click “Subscribe”"), else the tool's plain "would" wording ("Would write file", "Would use Gmail: send email").
321+
*/
322+
export function waitingSmallPrint(actionLine?: string | null, would?: string | null): string | null {
323+
if (actionLine?.trim()) return null
324+
return would?.trim() || null
325+
}

‎desktop/src/features/chat/toolMeta.ts‎

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,55 +47,56 @@ interface ToolMeta {
4747
would?: string
4848
}
4949

50-
const KNOWN: Record<string, ToolMeta> = {
51-
memory_recall: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
52-
memory_search: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
50+
/** Every tool the window names; each has a `would` line for waiting approvals and try runs (tests/approvalRisk.test.ts). */
51+
export const KNOWN: Record<string, ToolMeta> = {
52+
memory_recall: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
53+
memory_search: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
5354
memory_save: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
5455
memory_remember: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
5556
memory_add: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
5657
memory_forget: { running: 'Forgetting', done: 'Forgot', icon: IconBrain, would: 'Would forget' },
57-
memory_search_history: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory },
58-
memory_search_by_source: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
59-
history_semantic_search: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory },
60-
history_time_search: { running: 'Looking back in time', done: 'Looked back in time', icon: IconHistory },
61-
current_datetime: { running: 'Checking the date and time', done: 'Checked the date and time', icon: IconClock },
62-
file_list: { running: 'Listing files', done: 'Listed files', icon: IconFolder },
63-
file_read: { running: 'Reading file', done: 'Read file', icon: IconFile },
58+
memory_search_history: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory, would: 'Would search past chats' },
59+
memory_search_by_source: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
60+
history_semantic_search: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory, would: 'Would search past chats' },
61+
history_time_search: { running: 'Looking back in time', done: 'Looked back in time', icon: IconHistory, would: 'Would look back in time' },
62+
current_datetime: { running: 'Checking the date and time', done: 'Checked the date and time', icon: IconClock, would: 'Would check the date and time' },
63+
file_list: { running: 'Listing files', done: 'Listed files', icon: IconFolder, would: 'Would list files' },
64+
file_read: { running: 'Reading file', done: 'Read file', icon: IconFile, would: 'Would read a file' },
6465
file_write: { running: 'Writing file', done: 'Wrote file', icon: IconFilePencil, would: 'Would write file' },
65-
folder_search: { running: 'Searching your notes', done: 'Searched your notes', icon: IconNotebook },
66-
folder_read: { running: 'Reading a note', done: 'Read a note', icon: IconNotebook },
67-
time_now: { running: 'Checking the time', done: 'Checked the time', icon: IconClock },
68-
current_time: { running: 'Checking the time', done: 'Checked the time', icon: IconClock },
69-
skill_view: { running: 'Opening skill', done: 'Opened skill', icon: IconSparkles },
66+
folder_search: { running: 'Searching your notes', done: 'Searched your notes', icon: IconNotebook, would: 'Would search your notes' },
67+
folder_read: { running: 'Reading a note', done: 'Read a note', icon: IconNotebook, would: 'Would read a note' },
68+
time_now: { running: 'Checking the time', done: 'Checked the time', icon: IconClock, would: 'Would check the time' },
69+
current_time: { running: 'Checking the time', done: 'Checked the time', icon: IconClock, would: 'Would check the time' },
70+
skill_view: { running: 'Opening skill', done: 'Opened skill', icon: IconSparkles, would: 'Would open a skill' },
7071
skill_save: { running: 'Saving a skill', done: 'Saved a skill', icon: IconSparkles, would: 'Would save a skill' },
71-
skill_list: { running: 'Listing skills', done: 'Listed skills', icon: IconSparkles },
72-
web_search: { running: 'Searching the web', done: 'Searched the web', icon: IconWorld },
72+
skill_list: { running: 'Listing skills', done: 'Listed skills', icon: IconSparkles, would: 'Would list skills' },
73+
web_search: { running: 'Searching the web', done: 'Searched the web', icon: IconWorld, would: 'Would search the web' },
7374
// §10-13 code, helpers, browser and devices
7475
execute_code: { running: 'Running code', done: 'Ran code', icon: IconTerminal2, would: 'Would run code' },
7576
terminal_run: { running: 'Running a command', done: 'Ran a command', icon: IconTerminal2, would: 'Would run a command' },
7677
delegate_task: { running: 'Asking a helper', done: 'Asked a helper', icon: IconUserBolt, would: 'Would ask a helper' },
7778
delegate_tasks: { running: 'Asking helpers', done: 'Asked helpers', icon: IconUsersGroup, would: 'Would ask helpers' },
78-
read_with_reader: { running: 'Reading it with a safe reader', done: 'Read it with a safe reader', icon: IconShieldSearch },
79-
browser_open: { running: 'Opening a page', done: 'Opened a page', icon: IconWorldWww },
80-
browser_snapshot: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails },
79+
read_with_reader: { running: 'Reading it with a safe reader', done: 'Read it with a safe reader', icon: IconShieldSearch, would: 'Would read it with a safe reader' },
80+
browser_open: { running: 'Opening a page', done: 'Opened a page', icon: IconWorldWww, would: 'Would open a page' },
81+
browser_snapshot: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails, would: 'Would read the page' },
8182
browser_click: { running: 'Clicking', done: 'Clicked', icon: IconPointer, would: 'Would click' },
8283
browser_type: { running: 'Typing', done: 'Typed', icon: IconKeyboard, would: 'Would type' },
8384
browser_select: { running: 'Choosing an option', done: 'Chose an option', icon: IconListDetails, would: 'Would choose an option' },
8485
browser_press: { running: 'Pressing a key', done: 'Pressed a key', icon: IconKeyboard, would: 'Would press a key' },
85-
browser_scroll: { running: 'Scrolling', done: 'Scrolled', icon: IconArrowsVertical },
86-
browser_back: { running: 'Going back', done: 'Went back', icon: IconArrowBackUp },
87-
browser_tabs: { running: 'Checking tabs', done: 'Checked tabs', icon: IconAppWindow },
88-
browser_switch_tab: { running: 'Switching tabs', done: 'Switched tabs', icon: IconAppWindow },
89-
browser_extract: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails },
90-
browser_screenshot: { running: 'Taking a screenshot', done: 'Took a screenshot', icon: IconScreenshot },
91-
browser_close: { running: 'Closing the browser', done: 'Closed the browser', icon: IconWorld },
92-
device_list: { running: 'Checking your devices', done: 'Checked your devices', icon: IconDevices },
93-
device_take_photo: { running: 'Taking a photo', done: 'Took a photo', icon: IconCamera },
94-
device_capture_screen: { running: 'Looking at the screen', done: 'Looked at the screen', icon: IconScreenshot },
95-
device_get_location: { running: 'Finding your location', done: 'Found your location', icon: IconMapPin },
96-
device_notify: { running: 'Sending a notification', done: 'Sent a notification', icon: IconBellRinging },
97-
device_display: { running: 'Showing it on your device', done: 'Showed it on your device', icon: IconDevices },
98-
device_speak: { running: 'Speaking on your device', done: 'Spoke on your device', icon: IconSpeakerphone }
86+
browser_scroll: { running: 'Scrolling', done: 'Scrolled', icon: IconArrowsVertical, would: 'Would scroll' },
87+
browser_back: { running: 'Going back', done: 'Went back', icon: IconArrowBackUp, would: 'Would go back' },
88+
browser_tabs: { running: 'Checking tabs', done: 'Checked tabs', icon: IconAppWindow, would: 'Would check tabs' },
89+
browser_switch_tab: { running: 'Switching tabs', done: 'Switched tabs', icon: IconAppWindow, would: 'Would switch tabs' },
90+
browser_extract: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails, would: 'Would read the page' },
91+
browser_screenshot: { running: 'Taking a screenshot', done: 'Took a screenshot', icon: IconScreenshot, would: 'Would take a screenshot' },
92+
browser_close: { running: 'Closing the browser', done: 'Closed the browser', icon: IconWorld, would: 'Would close the browser' },
93+
device_list: { running: 'Checking your devices', done: 'Checked your devices', icon: IconDevices, would: 'Would check your devices' },
94+
device_take_photo: { running: 'Taking a photo', done: 'Took a photo', icon: IconCamera, would: 'Would take a photo' },
95+
device_capture_screen: { running: 'Looking at the screen', done: 'Looked at the screen', icon: IconScreenshot, would: 'Would look at the screen' },
96+
device_get_location: { running: 'Finding your location', done: 'Found your location', icon: IconMapPin, would: 'Would find your location' },
97+
device_notify: { running: 'Sending a notification', done: 'Sent a notification', icon: IconBellRinging, would: 'Would send a notification' },
98+
device_display: { running: 'Showing it on your device', done: 'Showed it on your device', icon: IconDevices, would: 'Would show it on your device' },
99+
device_speak: { running: 'Speaking on your device', done: 'Spoke on your device', icon: IconSpeakerphone, would: 'Would speak on your device' }
99100
}
100101

101102
const PREFIX: Array<[RegExp, Icon]> = [
@@ -138,6 +139,12 @@ export function wouldLabel(name: string): string {
138139
return `Would use ${meta.done}`
139140
}
140141

142+
/** What a waiting approval says the call would do, in plain words; null for a known tool without such wording
143+
* (its past-tense label would read "Would use Checked the date and time"). */
144+
export function askingLabel(name: string): string | null {
145+
return KNOWN[name] && !KNOWN[name].would ? null : wouldLabel(name)
146+
}
147+
141148
export function toolMeta(name: string): ToolMeta {
142149
const known = KNOWN[name]
143150
if (known) return known

‎desktop/src/hooks/core.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ export function useFiles() {
170170
return useQuery({ queryKey: qk.files, queryFn: api.files.list })
171171
}
172172

173+
/** Whether an attachment's file is still there (#458); only asked when `enabled`. */
174+
export function useFileExists(name: string, enabled: boolean) {
175+
return useQuery({ queryKey: qk.fileExists(name), queryFn: () => api.files.exists(name), enabled, staleTime: 60_000 })
176+
}
177+
173178
export function useTools() {
174179
return useQuery({ queryKey: qk.tools, queryFn: api.tools.list, staleTime: 5 * 60_000 })
175180
}

‎desktop/src/hooks/queryKeys.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export const qk = {
9191
usage: (days: number) => ['usage', days] as const,
9292
usageCost: ['usage', 'cost'] as const,
9393
files: ['files'] as const,
94+
fileExists: (name: string) => ['files', 'exists', name] as const,
9495
tools: ['tools'] as const,
9596

9697
/** The activity timeline (§21). Invalidated by most domain events (lib/events.ts). */

‎desktop/src/lib/api.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,16 @@ export const api = {
523523
list: () => http.get<FileEntry[]>('/api/files'),
524524
contentUrl: (name: string) => authedUrl(`/api/files/content/${encPath(name)}`),
525525
download: (name: string) => http.blob('GET', `/api/files/content/${encPath(name)}`),
526+
/** False when the file is gone (deleted, or went with an incognito chat), so a chat never shows another file (#458). */
527+
exists: async (name: string) => {
528+
try {
529+
await send('HEAD', `/api/files/content/${encPath(name)}`)
530+
return true
531+
} catch (err) {
532+
if (err instanceof ApiError && err.status === 404) return false
533+
throw err
534+
}
535+
},
526536
delete: (name: string) => http.delete<OkResponse>(`/api/files/${encPath(name)}`)
527537
},
528538

0 commit comments

Comments
 (0)