fix(cursor): surface agent errors with warning styling in web UI (#871)

* test: reproduce issue #864

Assert Cursor error paths emit agent error payloads and web UI renders
them as warning-styled events instead of neutral session messages.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): surface agent errors with warning styling in web UI (closes #864)

Route Cursor stderr, init, prompt, and legacy exit failures through
sendAgentMessage({ type: 'error' }) and teach the web chat layer to
render error events with a warning icon instead of neutral info text.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SSU-WEI HUANG
2026-06-18 10:17:57 +08:00
committed by GitHub
co-authored by Cursor
parent bfd0f4a376
commit 02a0aa6733
13 changed files with 149 additions and 24 deletions
+21
View File
@@ -178,6 +178,27 @@ describe('normalizeDecryptedMessage', () => {
})
})
it('normalizes agent error payloads as error events', () => {
const normalized = normalizeDecryptedMessage(makeMessage({
role: 'agent',
content: {
type: 'codex',
data: {
type: 'error',
message: 'Cursor Agent failed: authentication required'
}
}
}))
expect(normalized).toMatchObject({
role: 'event',
content: {
type: 'error',
message: 'Cursor Agent failed: authentication required'
}
})
})
it('treats non-sidechain string user output as sidechain', () => {
const message = makeMessage({
role: 'agent',
+15
View File
@@ -577,6 +577,21 @@ export function normalizeAgentRecord(
}
}
if (data.type === 'error' && typeof data.message === 'string') {
return {
id: messageId,
localId,
createdAt,
role: 'event',
content: {
type: 'error',
message: data.message
},
isSidechain: false,
meta
}
}
if (data.type === 'message' && typeof data.message === 'string') {
const review = parseCodexReviewMessage(data.message)
if (review) {
+12
View File
@@ -1,6 +1,18 @@
import { describe, expect, it } from 'vitest'
import { getEventPresentation, formatMessageTimestamp, formatResetTime } from './presentation'
describe('getEventPresentation — agent errors', () => {
it('formats error events with warning icon and message text', () => {
const result = getEventPresentation({
type: 'error',
message: 'Cursor Agent failed: authentication required'
})
expect(result.icon).toBe('⚠️')
expect(result.text).toBe('Cursor Agent failed: authentication required')
})
})
describe('getEventPresentation — limit-warning', () => {
it('formats five_hour warning', () => {
const result = getEventPresentation({
+3
View File
@@ -182,6 +182,9 @@ export function getEventPresentation(event: AgentEvent): EventPresentation {
const suffix = typeLabel ? ` (${typeLabel})` : ''
return { icon: '⏳', text: endsAt ? `Usage limit reached${suffix} until ${formatUnixTimestamp(endsAt)}` : `Usage limit reached${suffix}` }
}
if (event.type === 'error') {
return { icon: '⚠️', text: typeof event.message === 'string' ? event.message : 'Error' }
}
if (event.type === 'message') {
return { icon: null, text: typeof event.message === 'string' ? event.message : 'Message' }
}
+12
View File
@@ -79,6 +79,18 @@ export function dedupeAgentEvents(blocks: ChatBlock[]): ChatBlock[] {
continue
}
if (event.type === 'error' && typeof event.message === 'string') {
const message = event.message.trim()
const key = `error:${message}`
if (key === prevEventKey) {
continue
}
result.push(block)
prevEventKey = key
prevTitleChangedTo = null
continue
}
let key: string
try {
key = `event:${JSON.stringify(event)}`
+9
View File
@@ -180,6 +180,15 @@ function normalizeTraceMessage(
meta: source.meta
}
if (data.type === 'error' && typeof data.message === 'string') {
return [{
...base,
id: traceId,
role: 'event',
content: { type: 'error', message: data.message }
} as TracedMessage]
}
if (data.type === 'message' && typeof data.message === 'string') {
return [{
...base,
+1
View File
@@ -16,6 +16,7 @@ export type UsageData = {
export type AgentEvent =
| { type: 'switch'; mode: 'local' | 'remote' }
| { type: 'message'; message: string }
| { type: 'error'; message: string }
| { type: 'title-changed'; title: string }
| { type: 'limit-reached'; endsAt: number; limitType: string }
| { type: 'limit-warning'; /** 01 ratio (e.g. 0.9 = 90%), integer-precision via CLI pipe format */ utilization: number; endsAt: number; limitType: string }