Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes/435.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**A request waiting for your approval no longer reads as if it already happened.** A click waiting for your
answer showed a line like "Clicked using browser_click" under it. Now the card says only what Sentient would do,
in plain words.
11 changes: 3 additions & 8 deletions desktop/src/features/chat/ApprovalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import type { ApprovalDecision } from '@/lib/types'
import { cn, humanize, truncate } from '@/lib/utils'
import { ActionPreview } from './ActionPreview'
import { hostOf, refLabel } from './cards/bits'
import { toolMeta } from './toolMeta'
import { approvalCardReason, resolveWording, type RiskLabel } from './approvalRisk'
import { askingLabel } from './toolMeta'
import { approvalCardReason, resolveWording, waitingSmallPrint, type RiskLabel } from './approvalRisk'

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

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

return (
<motion.div
Expand Down
10 changes: 10 additions & 0 deletions desktop/src/features/chat/approvalRisk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,13 @@ export function approvalCardReason(reason?: string | null, badgeLabel?: string |
}
return trimmed
}

/**
* The small print of a waiting approval card that has no reason of its own. Nothing has happened yet, so never
* past tense and never the tool's developer name: nothing when the action line above already says what will happen
* ("Click “Subscribe”"), else the tool's plain "would" wording ("Would write file", "Would use Gmail: send email").
*/
export function waitingSmallPrint(actionLine?: string | null, would?: string | null): string | null {
if (actionLine?.trim()) return null
return would?.trim() || null
}
75 changes: 41 additions & 34 deletions desktop/src/features/chat/toolMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,55 +47,56 @@ interface ToolMeta {
would?: string
}

const KNOWN: Record<string, ToolMeta> = {
memory_recall: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
memory_search: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
/** Every tool the window names; each has a `would` line for waiting approvals and try runs (tests/approvalRisk.test.ts). */
export const KNOWN: Record<string, ToolMeta> = {
memory_recall: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
memory_search: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
memory_save: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
memory_remember: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
memory_add: { running: 'Saving to memory', done: 'Saved to memory', icon: IconBrain, would: 'Would save to memory' },
memory_forget: { running: 'Forgetting', done: 'Forgot', icon: IconBrain, would: 'Would forget' },
memory_search_history: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory },
memory_search_by_source: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain },
history_semantic_search: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory },
history_time_search: { running: 'Looking back in time', done: 'Looked back in time', icon: IconHistory },
current_datetime: { running: 'Checking the date and time', done: 'Checked the date and time', icon: IconClock },
file_list: { running: 'Listing files', done: 'Listed files', icon: IconFolder },
file_read: { running: 'Reading file', done: 'Read file', icon: IconFile },
memory_search_history: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory, would: 'Would search past chats' },
memory_search_by_source: { running: 'Searching memory', done: 'Searched memory', icon: IconBrain, would: 'Would search memory' },
history_semantic_search: { running: 'Searching past chats', done: 'Searched past chats', icon: IconHistory, would: 'Would search past chats' },
history_time_search: { running: 'Looking back in time', done: 'Looked back in time', icon: IconHistory, would: 'Would look back in time' },
current_datetime: { running: 'Checking the date and time', done: 'Checked the date and time', icon: IconClock, would: 'Would check the date and time' },
file_list: { running: 'Listing files', done: 'Listed files', icon: IconFolder, would: 'Would list files' },
file_read: { running: 'Reading file', done: 'Read file', icon: IconFile, would: 'Would read a file' },
file_write: { running: 'Writing file', done: 'Wrote file', icon: IconFilePencil, would: 'Would write file' },
folder_search: { running: 'Searching your notes', done: 'Searched your notes', icon: IconNotebook },
folder_read: { running: 'Reading a note', done: 'Read a note', icon: IconNotebook },
time_now: { running: 'Checking the time', done: 'Checked the time', icon: IconClock },
current_time: { running: 'Checking the time', done: 'Checked the time', icon: IconClock },
skill_view: { running: 'Opening skill', done: 'Opened skill', icon: IconSparkles },
folder_search: { running: 'Searching your notes', done: 'Searched your notes', icon: IconNotebook, would: 'Would search your notes' },
folder_read: { running: 'Reading a note', done: 'Read a note', icon: IconNotebook, would: 'Would read a note' },
time_now: { running: 'Checking the time', done: 'Checked the time', icon: IconClock, would: 'Would check the time' },
current_time: { running: 'Checking the time', done: 'Checked the time', icon: IconClock, would: 'Would check the time' },
skill_view: { running: 'Opening skill', done: 'Opened skill', icon: IconSparkles, would: 'Would open a skill' },
skill_save: { running: 'Saving a skill', done: 'Saved a skill', icon: IconSparkles, would: 'Would save a skill' },
skill_list: { running: 'Listing skills', done: 'Listed skills', icon: IconSparkles },
web_search: { running: 'Searching the web', done: 'Searched the web', icon: IconWorld },
skill_list: { running: 'Listing skills', done: 'Listed skills', icon: IconSparkles, would: 'Would list skills' },
web_search: { running: 'Searching the web', done: 'Searched the web', icon: IconWorld, would: 'Would search the web' },
// §10-13 code, helpers, browser and devices
execute_code: { running: 'Running code', done: 'Ran code', icon: IconTerminal2, would: 'Would run code' },
terminal_run: { running: 'Running a command', done: 'Ran a command', icon: IconTerminal2, would: 'Would run a command' },
delegate_task: { running: 'Asking a helper', done: 'Asked a helper', icon: IconUserBolt, would: 'Would ask a helper' },
delegate_tasks: { running: 'Asking helpers', done: 'Asked helpers', icon: IconUsersGroup, would: 'Would ask helpers' },
read_with_reader: { running: 'Reading it with a safe reader', done: 'Read it with a safe reader', icon: IconShieldSearch },
browser_open: { running: 'Opening a page', done: 'Opened a page', icon: IconWorldWww },
browser_snapshot: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails },
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' },
browser_open: { running: 'Opening a page', done: 'Opened a page', icon: IconWorldWww, would: 'Would open a page' },
browser_snapshot: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails, would: 'Would read the page' },
browser_click: { running: 'Clicking', done: 'Clicked', icon: IconPointer, would: 'Would click' },
browser_type: { running: 'Typing', done: 'Typed', icon: IconKeyboard, would: 'Would type' },
browser_select: { running: 'Choosing an option', done: 'Chose an option', icon: IconListDetails, would: 'Would choose an option' },
browser_press: { running: 'Pressing a key', done: 'Pressed a key', icon: IconKeyboard, would: 'Would press a key' },
browser_scroll: { running: 'Scrolling', done: 'Scrolled', icon: IconArrowsVertical },
browser_back: { running: 'Going back', done: 'Went back', icon: IconArrowBackUp },
browser_tabs: { running: 'Checking tabs', done: 'Checked tabs', icon: IconAppWindow },
browser_switch_tab: { running: 'Switching tabs', done: 'Switched tabs', icon: IconAppWindow },
browser_extract: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails },
browser_screenshot: { running: 'Taking a screenshot', done: 'Took a screenshot', icon: IconScreenshot },
browser_close: { running: 'Closing the browser', done: 'Closed the browser', icon: IconWorld },
device_list: { running: 'Checking your devices', done: 'Checked your devices', icon: IconDevices },
device_take_photo: { running: 'Taking a photo', done: 'Took a photo', icon: IconCamera },
device_capture_screen: { running: 'Looking at the screen', done: 'Looked at the screen', icon: IconScreenshot },
device_get_location: { running: 'Finding your location', done: 'Found your location', icon: IconMapPin },
device_notify: { running: 'Sending a notification', done: 'Sent a notification', icon: IconBellRinging },
device_display: { running: 'Showing it on your device', done: 'Showed it on your device', icon: IconDevices },
device_speak: { running: 'Speaking on your device', done: 'Spoke on your device', icon: IconSpeakerphone }
browser_scroll: { running: 'Scrolling', done: 'Scrolled', icon: IconArrowsVertical, would: 'Would scroll' },
browser_back: { running: 'Going back', done: 'Went back', icon: IconArrowBackUp, would: 'Would go back' },
browser_tabs: { running: 'Checking tabs', done: 'Checked tabs', icon: IconAppWindow, would: 'Would check tabs' },
browser_switch_tab: { running: 'Switching tabs', done: 'Switched tabs', icon: IconAppWindow, would: 'Would switch tabs' },
browser_extract: { running: 'Reading the page', done: 'Read the page', icon: IconListDetails, would: 'Would read the page' },
browser_screenshot: { running: 'Taking a screenshot', done: 'Took a screenshot', icon: IconScreenshot, would: 'Would take a screenshot' },
browser_close: { running: 'Closing the browser', done: 'Closed the browser', icon: IconWorld, would: 'Would close the browser' },
device_list: { running: 'Checking your devices', done: 'Checked your devices', icon: IconDevices, would: 'Would check your devices' },
device_take_photo: { running: 'Taking a photo', done: 'Took a photo', icon: IconCamera, would: 'Would take a photo' },
device_capture_screen: { running: 'Looking at the screen', done: 'Looked at the screen', icon: IconScreenshot, would: 'Would look at the screen' },
device_get_location: { running: 'Finding your location', done: 'Found your location', icon: IconMapPin, would: 'Would find your location' },
device_notify: { running: 'Sending a notification', done: 'Sent a notification', icon: IconBellRinging, would: 'Would send a notification' },
device_display: { running: 'Showing it on your device', done: 'Showed it on your device', icon: IconDevices, would: 'Would show it on your device' },
device_speak: { running: 'Speaking on your device', done: 'Spoke on your device', icon: IconSpeakerphone, would: 'Would speak on your device' }
}

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

/** What a waiting approval says the call would do, in plain words; null for a known tool without such wording
* (its past-tense label would read "Would use Checked the date and time"). */
export function askingLabel(name: string): string | null {
return KNOWN[name] && !KNOWN[name].would ? null : wouldLabel(name)
Comment thread
itsskofficial marked this conversation as resolved.
}

export function toolMeta(name: string): ToolMeta {
const known = KNOWN[name]
if (known) return known
Expand Down
32 changes: 31 additions & 1 deletion desktop/tests/approvalRisk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Run with `npm test` (no Electron needed).
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { approvalCardReason, approvalRisk, engineWording, resolveWording } from '../src/features/chat/approvalRisk.ts'
import { approvalCardReason, approvalRisk, engineWording, resolveWording, waitingSmallPrint } from '../src/features/chat/approvalRisk.ts'

const email = (name: string, body: string, reason = 'mcp_mailer: Send an email to one address') => ({
risk: 'send' as const,
Expand Down Expand Up @@ -351,3 +351,33 @@ test('a free Subscribe click shows "Signs up" once, a delete click keeps its own
assert.equal(resolveWording(remove).label, 'Deletes')
assert.equal(approvalCardReason(remove.reason, resolveWording(remove).label), 'Click "Delete account"')
})

// A waiting card with no reason of its own (every browser click since #404 and #412) used to read
// "Clicked using browser_click": past tense before anything happened, and a developer name.
test('a waiting approval card never says what already happened or names the tool', () => {
// the action line already says what will happen: no small print under it
assert.equal(waitingSmallPrint('Click “Subscribe”', 'Would click'), null)
assert.equal(waitingSmallPrint('Type “hello” and submit', 'Would type'), null)
// no action line: the tool's plain wording, about what would happen
assert.equal(waitingSmallPrint(null, 'Would write file'), 'Would write file')
assert.equal(waitingSmallPrint(undefined, 'Would send a notification'), 'Would send a notification')
assert.equal(waitingSmallPrint('', 'Would use Gmail: send email'), 'Would use Gmail: send email')
// nothing to say: no line at all
assert.equal(waitingSmallPrint(null, null), null)
assert.equal(waitingSmallPrint(' ', ' '), null)
})

// #450 review: a known tool without `would` wording left a waiting card with only its heading
// (a browser_screenshot under an Ask rule). Every known tool now says what it would do. toolMeta.ts imports
// through the `@/` alias, which node's test runner can't load, so this reads its table as text.
test('every known tool says what it would do while it waits', async () => {
const { readFileSync } = await import('node:fs')
const src = readFileSync(new URL('../src/features/chat/toolMeta.ts', import.meta.url), 'utf8')
const lines = src.slice(src.indexOf('export const KNOWN')).split(/\r?\n/)
const table = lines.slice(1, lines.findIndex((line) => line === '}'))
const entries = table.filter((line) => /^ {2}[a-z_]+: \{/.test(line))
assert.ok(entries.length > 40, 'found the tool table')
const missing = entries.filter((line) => !/would: '/.test(line)).map((line) => line.trim().split(':')[0])
assert.deepEqual(missing, [])
assert.ok(entries.some((line) => line.includes("browser_screenshot") && line.includes("would: 'Would take a screenshot'")))
})
Loading