Skip to content

Commit c184d0a

Browse files
committed
feat: performance and renderer improvements
1 parent 10e85f8 commit c184d0a

6 files changed

Lines changed: 55 additions & 33 deletions

File tree

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "desktop-intelligence",
3-
"version": "3.0.0-beta-2",
3+
"version": "3.0.0-beta-4",
44
"description": "Electron orchestration layer for local and cloud LLMs via LMStudio, Ollama, OpenRouter and Nvidia Build",
55
"main": "out/main/index.js",
66
"author": "Karan Grover",

‎progress.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,5 @@ append a row. Never delete rows. This is how you know what has and hasn't been d
292292
| 246 | 2026-06-07 | src/renderer/src/components/chat/MessageBubble.tsx, package.json | fix(ui): rail line opacity raised to 13% (was invisible at 7%), inter-row margins zeroed via wrapper divs inside RailSegment. Version 3.0.0-alpha-14 → 3.0.0-alpha-15. | ✅ Done |
293293
| 247 | 2026-06-07 | src/renderer/src/components/chat/MessageBubble.tsx, package.json | fix(ui): fix vertical alignment regression in rail rows — removed wrapper divs, use className="!mb-0" directly on each component to override mb-2 via Tailwind important modifier. Version 3.0.0-alpha-15 → 3.0.0-alpha-16. | ✅ Done |
294294
| 248 | 2026-06-07 | src/renderer/src/components/chat/MessageBubble.tsx, package.json | feat(ui): answer blocks break the rail inline in sequence — isPaneBlock renamed isBreakBlock, answer type added alongside stock_chart, segmentBlocks() simplified to return Segment[] directly, separate answerBlocks render path removed. Model commentary now appears inline between tool call groups preserving agentic sequence. Version 3.0.0-beta-1 → 3.0.0-beta-2. | ✅ Done |
295+
| 249 | 2026-06-07 | src/renderer/src/lib/markdownUtils.ts | fix(renderer): splitMarkdownIntoBlocks uses CommonMark-correct fence matching — replaces broken boolean toggle with fenceMinLen tracker (null = outside fence, number = min backtick run length needed to close). Prevents long responses with multiple code blocks from corrupting fence state and dropping trailing content mid-render. | ✅ Done |
296+
| 250 | 2026-06-07 | src/renderer/src/components/layout/InputBar.tsx, package.json | fix(perf): remove CSS @property --ib-angle which caused Chromium to tick the animation compositor at 60fps continuously even at complete idle — root cause of persistent M1 Pro stutter across hover, typing, and settings. Replaced pseudo-element conic-gradient animation (which required @property to drive from var) with two real DOM nodes (.ib-glow-border, .ib-glow-blur) using hardware-accelerated transform:rotate. .ib-glow-ring gains overflow:hidden to clip the oversized rotating divs. .ib-glow-inner gains margin:1px replacing the old padding:1px on the ring. Visual output is identical. Version 3.0.0-beta-3 → 3.0.0-beta-4. | ✅ Done |

‎src/renderer/src/components/chat/MessageBubble.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -522,11 +522,12 @@ function AssistantBubble({
522522
}
523523
if (block.type === 'answer') {
524524
return (
525-
<MarkdownRenderer
526-
key={block.id}
527-
content={block.content}
528-
isStreaming={block.isStreaming}
529-
/>
525+
<div key={block.id} style={{ marginTop: '0.6rem', marginBottom: '0.8rem' }}>
526+
<MarkdownRenderer
527+
content={block.content}
528+
isStreaming={block.isStreaming}
529+
/>
530+
</div>
530531
)
531532
}
532533
return null

‎src/renderer/src/components/layout/InputBar.tsx‎

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -134,26 +134,24 @@ export const InputBar = memo(function InputBar({
134134
from { opacity: 0; transform: scale(0.85) translateY(4px); }
135135
to { opacity: 1; transform: scale(1) translateY(0); }
136136
}
137-
@property --ib-angle {
138-
syntax: '<angle>';
139-
initial-value: 0deg;
140-
inherits: false;
141-
}
142137
@keyframes ib-revolve {
143-
to { --ib-angle: 360deg; }
138+
to { transform: translate(-50%, -50%) rotate(360deg); }
144139
}
145140
.ib-glow-ring {
146141
border-radius: 17px;
147142
padding: 1px;
148143
position: relative;
144+
overflow: hidden;
149145
}
150-
.ib-glow-ring::before {
151-
content: '';
146+
.ib-glow-border {
152147
position: absolute;
153-
inset: 0;
154-
border-radius: 17px;
148+
top: 50%;
149+
left: 50%;
150+
width: 1400px;
151+
height: 1400px;
152+
transform: translate(-50%, -50%) rotate(0deg);
155153
background: conic-gradient(
156-
from var(--ib-angle),
154+
from 0deg,
157155
transparent 0deg,
158156
rgba(229,57,53,0.0) 50deg,
159157
rgba(229,57,53,0.5) 120deg,
@@ -163,20 +161,18 @@ export const InputBar = memo(function InputBar({
163161
transparent 360deg
164162
);
165163
animation: ib-revolve 1.25s linear infinite;
166-
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
167-
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
168-
-webkit-mask-composite: xor;
169-
mask-composite: exclude;
170-
padding: 1px;
171164
z-index: 0;
165+
pointer-events: none;
172166
}
173-
.ib-glow-ring::after {
174-
content: '';
167+
.ib-glow-blur {
175168
position: absolute;
176-
inset: -2px;
177-
border-radius: 20px;
169+
top: 50%;
170+
left: 50%;
171+
width: 1400px;
172+
height: 1400px;
173+
transform: translate(-50%, -50%) rotate(0deg);
178174
background: conic-gradient(
179-
from var(--ib-angle),
175+
from 0deg,
180176
transparent 0deg,
181177
rgba(229,57,53,0.0) 50deg,
182178
rgba(229,57,53,0.10) 120deg,
@@ -188,13 +184,15 @@ export const InputBar = memo(function InputBar({
188184
animation: ib-revolve 1.25s linear infinite;
189185
filter: blur(8px);
190186
z-index: 0;
187+
pointer-events: none;
191188
}
192189
.ib-glow-inner {
193190
border-radius: 16px;
194191
background: #141414;
195192
overflow: hidden;
196193
position: relative;
197194
z-index: 1;
195+
margin: 1px;
198196
}
199197
`
200198
document.head.appendChild(s)
@@ -439,7 +437,12 @@ export const InputBar = memo(function InputBar({
439437
>
440438
{isStreaming ? (
441439
// ── Streaming: revolving conic-gradient glow ring ──
440+
// Uses transform:rotate on real DOM nodes instead of @property --ib-angle
441+
// to avoid the Chromium bug where a globally-registered @property with
442+
// syntax:'<angle>' keeps the compositor ticking at 60fps even at idle.
442443
<div className="ib-glow-ring">
444+
<div className="ib-glow-blur" aria-hidden="true" />
445+
<div className="ib-glow-border" aria-hidden="true" />
443446
<div className="ib-glow-inner relative">
444447
{innerContent}
445448
</div>

‎src/renderer/src/lib/markdownUtils.ts‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,20 +407,36 @@ export function prepareUserContent(raw: string): string {
407407
/**
408408
* Splits a markdown answer string by blank lines (\n\n) outside active code blocks,
409409
* allowing the renderer to memoize completed blocks and only re-parse the growing active one.
410+
*
411+
* Uses CommonMark-correct fence matching: the opening fence's backtick run length
412+
* is remembered, and only a closing line with AT LEAST that many backticks ends
413+
* the block. This prevents a simple toggle from being tripped by backtick
414+
* sequences inside fences (e.g. inline ` ``` ` inside a code example) or by
415+
* a partial fence at a streaming chunk boundary.
410416
*/
411417
export function splitMarkdownIntoBlocks(markdown: string): string[] {
412418
if (!markdown) return []
413419
const lines = markdown.split('\n')
414420
const blocks: string[] = []
415421
let currentBlock: string[] = []
416-
let inCodeBlock = false
422+
// null = not inside a fence; number = min backtick run needed to close
423+
let fenceMinLen: number | null = null
417424

418425
for (const line of lines) {
419-
if (/^\s*`{3,}/.test(line)) {
420-
inCodeBlock = !inCodeBlock
426+
const fenceMatch = line.match(/^(\s*)(`{3,}|~{3,})/)
427+
if (fenceMatch) {
428+
const runLen = fenceMatch[2].length
429+
if (fenceMinLen === null) {
430+
// Opening fence — remember minimum closing run length
431+
fenceMinLen = runLen
432+
} else if (runLen >= fenceMinLen && line.trim().replace(/`+|~+/g, '').trim() === '') {
433+
// Closing fence — must be at least as long as the opener AND have no
434+
// trailing info-string (info-string means it's a new opening fence)
435+
fenceMinLen = null
436+
}
421437
}
422438

423-
if (!inCodeBlock && line.trim() === '') {
439+
if (fenceMinLen === null && line.trim() === '') {
424440
if (currentBlock.length > 0) {
425441
blocks.push(currentBlock.join('\n'))
426442
currentBlock = []

0 commit comments

Comments
 (0)