feat(desktop): theme polish, prose chat typography, composer chrome

- DS tokens/midground, Backdrop, scoped scrollbars, typography plugin + prose
- Composer liquid/radius utilities, thread font parity, tool/thinking cues
- File tree label scale, preview flex, thread retry loading + streaming tests
This commit is contained in:
Brooklyn Nicholson
2026-05-11 10:25:23 -04:00
parent 61fb5a48b7
commit bff052d61f
32 changed files with 1180 additions and 676 deletions
+8 -2
View File
@@ -39,6 +39,7 @@ interface CodeSignals {
hasMarkdown: boolean
proseLines: number
trimmed: string
urlLines: number
}
export function sanitizeLanguageTag(tag: string): string {
@@ -75,7 +76,8 @@ function codeSignals(body: string): CodeSignals {
codeSignals: codeSignalCount(trimmed),
hasMarkdown: markdownSignals > 0,
proseLines: proseLineCount(trimmed),
trimmed
trimmed,
urlLines: (trimmed.match(/^\s*https?:\/\/\S+\s*$/gim) || []).length
}
}
@@ -96,7 +98,11 @@ export function isLikelyProseFence(info: string, body: string): boolean {
return false
}
if (hasInfoTail && signals.codeSignals <= 2 && (signals.proseLines >= 2 || signals.bulletLines >= 1)) {
if (
hasInfoTail &&
signals.codeSignals <= 2 &&
(signals.proseLines >= 2 || signals.bulletLines >= 1 || signals.urlLines >= 1)
) {
return true
}
+70 -28
View File
@@ -1,36 +1,19 @@
import { isLikelyProseFence, sanitizeLanguageTag } from '@/lib/markdown-code'
import { stripPreviewTargets } from '@/lib/preview-targets'
/**
* Strip provider/model "thinking" blocks before markdown render.
*
* Some Hermes providers stream raw `<think>…</think>` and similar into
* assistant text. Proper reasoning UI uses dedicated `reasoning.*` parts.
*/
const REASONING_BLOCK_RE = /<(think|thinking|reasoning|scratchpad|analysis)>[\s\S]*?<\/\1>\s*/gi
const PREVIEW_MARKER_RE = /\[Preview:[^\]]+\]\(#preview[:/][^)]+\)/gi
const FENCE_LINE_RE = /^([ \t]*)(`{3,}|~{3,})([^\n]*)$/
const EMPTY_FENCE_BLOCK_RE = /(^|\n)[ \t]*(?:`{3,}|~{3,})[^\n]*\n[ \t]*(?:`{3,}|~{3,})[ \t]*(?=\n|$)/g
const CODE_FENCE_SPLIT_RE = /((?:```|~~~)[\s\S]*?(?:```|~~~))/g
const INLINE_CODE_SPLIT_RE = /(`[^`\n]+`)/g
const RAW_URL_RE = /https?:\/\/[^\s<>"'`]+[^\s<>"'`.,;:!?]/g
const LOCAL_PREVIEW_URL_RE = /(^|\s)https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?[^\s<>"'`]*/gi
const LOCAL_PREVIEW_ONLY_RE = /^https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?\/?$/i
const URL_ONLY_LINE_RE = /^\s*https?:\/\/\S+\s*$/i
const CITATION_MARKER_RE = /(?<=[\p{L}\p{N})\].,!?:;"'])\[(?:\d+(?:\s*,\s*\d+)*)\](?!\()/gu
/**
* Forcefully scrub backtick noise the model streams that doesn't form a
* valid markdown construct.
*
* Strategy: find every well-formed `\`\`\`…\`\`\`` (or `~~~`) block whose
* opener and closer are each on their own line, snapshot those ranges as
* "protected", then nuke all other triple-backtick runs in the text.
* This handles every failure mode in one shot:
*
* - mid-line `\`\`\`` (`there:\`\`\` cd /Users/...`)
* - orphan opener with no closer
* - orphan closer with no opener
* - chunk-boundary backticks where the preceding non-newline char isn't
* in the same regex window
* - empty `` `` `` `` and orphan double backticks
*
* Real, well-formed fenced blocks survive untouched.
*/
function scrubBacktickNoise(text: string): string {
const balancedFenceRe = /(^|\n)([ \t]*)(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)\n[ \t]*\3[ \t]*(?=\n|$)/g
const protectedRanges: { end: number; start: number }[] = []
@@ -42,6 +25,24 @@ function scrubBacktickNoise(text: string): string {
protectedRanges.push({ end: balancedFenceRe.lastIndex, start })
}
const danglingCodeFenceRe = /(^|\n)[ \t]*(`{3,}|~{3,})([a-z0-9][a-z0-9+#-]{0,15})[ \t]*\n([\s\S]*)$/gi
while ((match = danglingCodeFenceRe.exec(text)) !== null) {
const start = match.index + match[1].length
const marker = match[2] || '```'
const info = match[3] || ''
const body = match[4] || ''
const closeRe = new RegExp(`\\n[ \\t]*${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[ \\t]*(?=\\n|$)`)
if (!closeRe.test(body) && sanitizeLanguageTag(info) && !isLikelyProseFence(info, body)) {
protectedRanges.push({ end: text.length, start })
break
}
}
protectedRanges.sort((a, b) => a.start - b.start)
const fenceNoiseRe = /`{3,}/g
let out = ''
let cursor = 0
@@ -54,8 +55,6 @@ function scrubBacktickNoise(text: string): string {
out += text.slice(cursor).replace(fenceNoiseRe, '')
// Empty inline code spans (`` `` `` `` with nothing meaningful inside)
// render as literal backticks. Two passes catch chains.
for (let pass = 0; pass < 2; pass += 1) {
out = out.replace(/``\s*``/g, '')
out = out.replace(/(^|[^`])``(?=\s|[.,;:!?)\]'"\u2014\u2013-]|$)/g, '$1')
@@ -68,6 +67,36 @@ function stripEmptyFenceBlocks(text: string): string {
return text.replace(EMPTY_FENCE_BLOCK_RE, '$1')
}
function isUrlOnlyBlock(lines: string[]): boolean {
const nonEmpty = lines.filter(line => line.trim())
return nonEmpty.length > 0 && nonEmpty.every(line => URL_ONLY_LINE_RE.test(line))
}
function autoLinkRawUrls(text: string): string {
return text.replace(RAW_URL_RE, (url: string, index: number) => {
const previous = text[index - 1] || ''
const beforePrevious = text[index - 2] || ''
if (previous === '<' || (beforePrevious === ']' && previous === '(')) {
return url
}
return `<${url}>`
})
}
function normalizeVisibleProse(text: string): string {
return text
.split(INLINE_CODE_SPLIT_RE)
.map(part =>
part.startsWith('`')
? part
: autoLinkRawUrls(part.replace(/`{3,}/g, '').replace(LOCAL_PREVIEW_URL_RE, '$1').replace(CITATION_MARKER_RE, ''))
)
.join('')
}
function pushProseFence(out: string[], indent: string, info: string, lines: string[]) {
if (info) {
out.push(`${indent}${info}`.trimEnd())
@@ -135,6 +164,19 @@ function normalizeFenceBlocks(text: string): string {
continue
}
if (closeIndex !== -1 && LOCAL_PREVIEW_ONLY_RE.test(body.trim())) {
index = closeIndex + 1
continue
}
if (closeIndex !== -1 && isUrlOnlyBlock(bodyLines)) {
out.push(...bodyLines)
index = closeIndex + 1
continue
}
if (closeIndex === -1) {
if (!body.trim()) {
index += 1
@@ -175,8 +217,8 @@ export function preprocessMarkdown(text: string): string {
const strippedEmptyFences = stripEmptyFenceBlocks(normalizedFences)
return strippedEmptyFences
.split(/((?:```|~~~)[\s\S]*?(?:```|~~~))/g)
.map(part => (/^(?:```|~~~)/.test(part) ? part : stripPreviewTargets(part)))
.split(CODE_FENCE_SPLIT_RE)
.map(part => (/^(?:```|~~~)/.test(part) ? part : normalizeVisibleProse(stripPreviewTargets(part))))
.join('')
.replace(/[ \t]+\n/g, '\n')
}