feat: more ui qa

This commit is contained in:
Brooklyn Nicholson
2026-05-16 21:26:50 -05:00
parent 64ab17182a
commit c7e6a48bfb
84 changed files with 939 additions and 1120 deletions
+5 -1
View File
@@ -318,7 +318,11 @@ export function toRuntimeMessage(message: ChatMessage): ThreadMessage {
role,
content: message.parts as Extract<ThreadMessage, { role: 'assistant' }>['content'],
createdAt,
status: message.pending ? { type: 'running' } : { type: 'complete', reason: 'stop' },
status: message.error
? { type: 'incomplete', reason: 'error', error: message.error }
: message.pending
? { type: 'running' }
: { type: 'complete', reason: 'stop' },
metadata: {
unstable_state: null,
unstable_annotations: [],
@@ -143,6 +143,7 @@ describe('external link helpers', () => {
it('ignores error-like fetched titles and falls back to slug label', async () => {
const bridge = vi.fn().mockResolvedValue('GetYourGuide Error')
installDesktopBridge({ fetchLinkTitle: bridge as unknown as Window['hermesDesktop']['fetchLinkTitle'] })
const url =
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
+1 -1
View File
@@ -216,7 +216,7 @@ export function ExternalLink({
return (
<a
className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current', className)}
className={cn('font-semibold text-foreground underline underline-offset-4 decoration-current/20', className)}
href={target}
onClick={event => {
event.stopPropagation()
+3 -13
View File
@@ -180,9 +180,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable {
return () =>
function transform(tree: Root, file: VFile): undefined {
visitParents(tree, 'element', (element, parents) => {
const classes = Array.isArray(element.properties?.className)
? (element.properties.className as string[])
: []
const classes = Array.isArray(element.properties?.className) ? (element.properties.className as string[]) : []
// Match the same class set rehype-katex looks for. `language-math`
// is the markdown ` ```math ` form, `math-inline` is what
@@ -201,12 +199,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable {
// For ` ```math ` the scope walks up to the wrapping <pre> and
// we treat it as display math. Same logic rehype-katex uses.
if (
languageMath &&
parent &&
parent.type === 'element' &&
(parent as Element).tagName === 'pre'
) {
if (languageMath && parent && parent.type === 'element' && (parent as Element).tagName === 'pre') {
scope = parent as Element
parent = parents[parents.length - 2]
displayMode = true
@@ -253,10 +246,7 @@ function createMemoizedRehypeKatex(options: KatexMemoOptions = {}): Pluggable {
* wrapper. Drop-in for `@streamdown/math`'s `createMathPlugin`.
*/
export function createMemoizedMathPlugin(config: MathPluginConfig = {}) {
const remarkPlugin: Pluggable = [
remarkMath,
{ singleDollarTextMath: config.singleDollarTextMath ?? false }
]
const remarkPlugin: Pluggable = [remarkMath, { singleDollarTextMath: config.singleDollarTextMath ?? false }]
const rehypePlugin = createMemoizedRehypeKatex({ errorColor: config.errorColor })
+10 -3
View File
@@ -310,7 +310,9 @@ const LATEX_INLINE_RE = /\\\(([^\n]+?)\\\)/g
const LATEX_DISPLAY_RE = /\\\[([\s\S]+?)\\\]/g
function rewriteLatexBracketDelimiters(text: string): string {
return text.replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`).replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
return text
.replace(LATEX_INLINE_RE, (_, body: string) => `$${body}$`)
.replace(LATEX_DISPLAY_RE, (_, body: string) => `$$${body}$$`)
}
// Escape `$<digit>` patterns so they don't get eaten as math delimiters.
@@ -340,14 +342,19 @@ export function preprocessMarkdown(text: string): string {
.split(CODE_FENCE_SPLIT_RE)
.map(part => {
// Fence blocks pass through untouched.
if (/^(?:```|~~~)/.test(part)) {return part}
if (/^(?:```|~~~)/.test(part)) {
return part
}
// Whitespace-only segments (e.g. the `\n\n` between two adjacent
// fences) must NOT go through stripPreviewTargets — its internal
// .trim() would collapse them to '' and glue the surrounding
// fences together, producing things like ``````math which the
// markdown parser then reads as a single 6-backtick block.
if (!part.trim()) {return part}
if (!part.trim()) {
return part
}
// Preserve leading/trailing whitespace around the prose body so
// that fence-prose-fence sequences keep their blank-line gaps.
// stripPreviewTargets internally calls .trim() on its result for
@@ -4,16 +4,16 @@ import { isProviderSetupErrorMessage } from './provider-setup-errors'
describe('isProviderSetupErrorMessage', () => {
it('matches generic missing-provider copy', () => {
expect(isProviderSetupErrorMessage('No inference provider configured. Run `hermes model` to choose one.')).toBe(true)
expect(isProviderSetupErrorMessage('No inference provider configured. Run `hermes model` to choose one.')).toBe(
true
)
expect(isProviderSetupErrorMessage('No inference provider is configured.')).toBe(true)
expect(isProviderSetupErrorMessage('set an API key (OPENROUTER_API_KEY) in ~/.hermes/.env')).toBe(true)
})
it('does not match non-provider runtime failures', () => {
expect(
isProviderSetupErrorMessage(
'Selected runtime is not available. setup.status reports configured credentials.'
)
isProviderSetupErrorMessage('Selected runtime is not available. setup.status reports configured credentials.')
).toBe(false)
})
+1
View File
@@ -16,6 +16,7 @@ function parseArray(value: unknown[]): TodoItem[] {
if (!isRecord(item) || !isStatus(item.status)) {
return []
}
const id = String(item.id ?? '').trim()
const content = String(item.content ?? '').trim()
+24 -4
View File
@@ -2,6 +2,7 @@
// mode still gets the raw JSON section.
const WRAPPER_KEYS = ['data', 'result', 'output', 'response', 'payload'] as const
const PRIORITY_KEYS = [
'title',
'name',
@@ -17,6 +18,7 @@ const PRIORITY_KEYS = [
'summary',
'description'
] as const
const ERROR_KEYS = ['error', 'errors', 'failure', 'exception'] as const
const ERROR_MSG_KEYS = ['message', 'reason', 'detail', 'stderr'] as const
const NON_ERROR_TEXT = new Set(['', '0', 'false', 'none', 'null', 'nil', 'ok', 'success', 'n/a', 'na'])
@@ -66,6 +68,7 @@ function clipBlock(value: string, maxChars = 1800, maxLines = 18): string {
if (!t) {
return ''
}
const lines = t.split('\n')
let text = lines.slice(0, maxLines).join('\n')
const clipped = lines.length > maxLines || text.length > maxChars
@@ -187,11 +190,13 @@ function formatFieldValue(value: unknown, depth: number): string {
if (!v.length) {
return ''
}
const scalars = v.map(summarizeScalar).filter(Boolean)
if (scalars.length === v.length && v.length <= 4) {
return clipInline(scalars.join(', '))
}
const first = summarizeListItem(v[0], depth + 1)
return first ? `${pluralize(v.length, 'item')} (${first})` : pluralize(v.length, 'item')
@@ -207,16 +212,21 @@ function formatFieldValue(value: unknown, depth: number): string {
// "Returned N items" / "0 items" / "Returned an empty object" are all
// noise — better to render nothing and let the title carry the signal.
function formatArraySummary(value: unknown[], depth: number): string {
if (!value.length) return ''
if (!value.length) {
return ''
}
const max = 6
const lines = value
.slice(0, max)
.map(item => summarizeListItem(item, depth + 1))
.filter(Boolean)
.map(l => `- ${l}`)
if (!lines.length) return ''
if (!lines.length) {
return ''
}
if (value.length > max) {
const remaining = value.length - max
@@ -228,7 +238,10 @@ function formatArraySummary(value: unknown[], depth: number): string {
function formatRecordSummary(record: Json, depth: number): string {
const keys = Object.keys(record)
if (!keys.length) return ''
if (!keys.length) {
return ''
}
if (depth <= 2) {
const direct = firstString(record, ['message', 'summary', 'description', 'preview', 'text', 'content'])
@@ -249,6 +262,7 @@ function formatRecordSummary(record: Json, depth: number): string {
if (!v) {
continue
}
lines.push(`- ${titleCase(k)}: ${v}`)
if (lines.length >= max) {
@@ -256,7 +270,9 @@ function formatRecordSummary(record: Json, depth: number): string {
}
}
if (!lines.length) return ''
if (!lines.length) {
return ''
}
if (candidates.length > lines.length) {
const remaining = candidates.length - lines.length
@@ -270,6 +286,7 @@ function formatSummaryValue(value: unknown, depth: number): string {
if (depth > 4) {
return ''
}
const v = norm(value)
if (typeof v === 'string') {
@@ -383,11 +400,13 @@ function findNestedError(value: unknown, depth: number, seen: Set<unknown>): str
if (depth > 5) {
return ''
}
const v = norm(value)
if (!v || typeof v !== 'object' || seen.has(v)) {
return ''
}
seen.add(v)
if (Array.isArray(v)) {
@@ -408,6 +427,7 @@ function findNestedError(value: unknown, depth: number, seen: Set<unknown>): str
if (!hasMeaningfulErrorValue(record[k])) {
continue
}
const text = valueErrorText(record[k])
if (text) {