Skip to content

Add model-eval Tools, theme customizer, and Labs UI polish - #9

Merged
Nostromo-618 merged 3 commits into
mainfrom
vdl-model-eval-theme-tools
Aug 8, 2026
Merged

Add model-eval Tools, theme customizer, and Labs UI polish#9
Nostromo-618 merged 3 commits into
mainfrom
vdl-model-eval-theme-tools

Conversation

@Nostromo-618

Copy link
Copy Markdown
Member

Summary

  • Add model-eval under Tools (runner, suite, report artifacts, harness, and catalog/eval UI) so Labs can compare models and surface results in-app.
  • Ship theme customizer with vdl-* localStorage remap/defaults so Labs prefs stay namespaced and restore cleanly.
  • Add rotating home quotes, catalog/eval UI polish, and a chat select chevron workaround; bump Node engines to >=20.9.0 for sharp@0.35.

Test plan

  • pnpm test (Playwright unit suite) passes locally
  • Open Labs home: rotating quote appears; Tools nav includes Model eval
  • Theme customizer: change prefs, reload — vdl-* keys persist; reset uses Labs defaults
  • Model eval Tools view loads report/harness; scorers behave as expected
  • AI chat: model/provider selects show chevron correctly; chat still focuses composer as before

Made with Cursor

Nostromo-618 and others added 2 commits August 8, 2026 16:55
Co-authored-by: Cursor <cursoragent@cursor.com>
…e quotes, and chat/eval UI polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread ai-chat.js Outdated
let out = String(text);
// Drop full thought channels if the runtime leaked them into content.
out = out.replace(/<\|channel>thought[\s\S]*?<channel\|>/gi, '');
out = out.replace(/<\|think\|>[\s\S]*?(?:<\|\/think\|>|$)/gi, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: $ anchor in think-tag regex destroys all trailing streaming content

The regex /<\|think\|>[\s\S]*?(?:<\|\/think\|>|$)/gi uses $ as an alternation fallback for unclosed <|think|> tags. During streaming, when a partial chunk contains an opening <|think|> without a matching </|think|> closing tag, the lazy [\s\S]*? combined with $ consumes and deletes ALL text from the opening tag through the end of the string. This silently destroys entire model responses.

Fix by removing the $ fallback and instead stripping orphan opening tags on a separate pass:

Suggested change
out = out.replace(/<\|think\|>[\s\S]*?(?:<\|\/think\|>|$)/gi, '');
out = out.replace(/<\|think\|>[\s\S]*?<\|\/think\|>/gi, '');

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread ai-chat.js Outdated
// Drop full thought channels if the runtime leaked them into content.
out = out.replace(/<\|channel>thought[\s\S]*?<channel\|>/gi, '');
out = out.replace(/<\|think\|>[\s\S]*?(?:<\|\/think\|>|$)/gi, '');
out = out.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: $ anchor in HTML think-tag regex destroys all trailing streaming content

Same bug as line 324 but for the HTML-style <think> tags. An unclosed <think> tag in a streaming chunk will cause [\s\S]*? with the $ fallback to consume and delete everything to end-of-string.

Suggested change
out = out.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, '');
out = out.replace(/<think>[\s\S]*?<\/think>/gi, '');

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread src/vdl-home-quotes.js Outdated

let salt = safeGet(storage, keys.salt);
if (!salt) {
salt = 'test-salt';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Hardcoded 'test-salt' instead of random salt in ensureHomeQuoteBag

ensureHomeQuoteBag sets salt = 'test-salt' when no salt is stored — a clear leftover debug artifact. This is the peek path called before pickNextHomeQuote, making every browser session that hits this path first get the same deterministic shuffle bag for the entire day, breaking the session-salt randomness guarantee.

Suggested change
salt = 'test-salt';
salt = randomSalt();

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread ai-chat.js
model: modelSource,
mainExecutorSettings: { maxNumTokens: 4096 },
mainExecutorSettings: {
maxNumTokens: option?.maxNumTokens || 4096,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: || 4096 treats 0 as falsy — use ?? for maxNumTokens fallback

option?.maxNumTokens || 4096 would incorrectly fall back to 4096 if a future model entry explicitly sets maxNumTokens: 0. While no current model has this, using nullish coalescing is more correct:

Suggested change
maxNumTokens: option?.maxNumTokens || 4096,
maxNumTokens: option?.maxNumTokens ?? 4096,

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread model-eval.js
* @param {string} reply
*/
export function scoreCase(testCase, reply) {
switch (testCase.scorer) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: scoreCase has no null/undefined guard on testCase parameter

switch (testCase.scorer) throws a TypeError if testCase is null or undefined. As a public export called by external harness code, it should validate its input:

Suggested change
switch (testCase.scorer) {
if (!testCase) return { pass: false, reasons: ['missing test case'] };
switch (testCase.scorer) {

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread model-eval.js
let used = 0;

for (const model of sorted) {
const size = model.approxBytes || GiB;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: || GiB treats approxBytes: 0 as falsy, defaulting to 1 GiB

model.approxBytes || GiB uses logical OR, so approxBytes: 0 (a valid size for a tiny model) evaluates to 0 || GiB = 1,073,741,824 bytes. Use nullish coalescing:

Suggested change
const size = model.approxBytes || GiB;
const size = model.approxBytes ?? GiB;

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

args: [
'--enable-unsafe-webgpu',
'--ignore-gpu-blocklist',
'--disable-web-security',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: --disable-web-security disables all CORS and origin protections

This Chromium flag removes all cross-origin security in the headed browser instance used for eval. While mitigated by an isolated profile at .models/.model-eval-profile, this flag should be carefully documented with a warning comment and only enabled when strictly necessary (vs. the existing --disable-web-security requirement may be avoidable with proper CORS headers from Vite).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

credentials: 'same-origin',
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
report.value = await res.json();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: No JSON shape validation after fetch — non-array models will crash rendering

report.value = await res.json() assigns parsed JSON directly without validating that report.models is an array. If the server returns malformed JSON where models is truthy but non-array (e.g. a string or object), downstream computed properties calling .map()/.filter() on report.models will throw a runtime error, silently crashing the component with a blank page.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if (typeof chat.reset === 'function') {
await chat.reset();
} else {
chat.messages = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Direct mutation of chat.messages = [] bypasses encapsulation

Assigning directly to an internal AiChat property skips intended side effects (state tracking, event emission, resource management). If AiChat changes its internal representation or adds getter/setter logic, this assignment will leave the instance in an inconsistent state. Prefer using chat.reset() (which already exists in the codebase) or add a dedicated public method for clearing messages.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Merge (minor suggestion only)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
ai-chat.js 329 Missing i flag on <|think|> orphan regex — inconsistent with other case-insensitive think matches
Files Reviewed (5 files)
  • ai-chat.js - 1 issue (sanitize fix: removed $-to-EOF, added export, orphan stripping)
  • doc/vdl-model-eval.md - reviewed (Chromium runner documentation added)
  • src/vdl-home-quotes.js - reviewed (salt fix: 'test-salt'randomSalt())
  • tests/unit/guardrails.spec.ts - reviewed (new sanitizeModelReply streaming tests)
  • utils/model-eval-runner.mjs - reviewed (inline docs for --disable-web-security flag)

Fix these issues in Kilo Cloud

Previous Review Summary (commit a775f09)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit a775f09)

Status: 9 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 3
WARNING 6
SUGGESTION 10
Issue Details (click to expand)

CRITICAL

File Line Issue
ai-chat.js 324 $ anchor in think-tag regex destroys all trailing streaming content when think tag is unclosed
ai-chat.js 325 Same $ anchor bug for HTML-style <think> tags
src/vdl-home-quotes.js 506 Hardcoded 'test-salt' in ensureHomeQuoteBag breaks session randomness

WARNING

File Line Issue
ai-chat.js 941 || should be ?? for maxNumTokens fallback (treats 0 as falsy)
model-eval.js 95 scoreCase has no null guard on testCase parameter
model-eval.js 126 || GiB should be ?? GiB for approxBytes fallback
utils/model-eval-runner.mjs 43 --disable-web-security flag disables all CORS protections
src/components/VdlModelEvalUI.vue 100 No JSON shape validation after fetch — non-array models crash rendering
src/demos/model-eval-harness.js 69 Direct mutation of chat.messages bypasses encapsulation

SUGGESTION

File Line Issue
ai-chat.js 273 Redundant gemma4 checks in generationConfigForModel
model-eval.js 302 summarizeModelResults accesses meta.modelId without null guard
model-eval.js 103 Unknown scorer returns indistinguishable pass: false result
src/vdl-home-quotes.js 242 normalizeHomeQuote produces 'null'/'undefined' string literals for null/undefined input
src/vdl-home-quotes.js 324 buildQuoteBag uses raw seed modulo instead of seeded PRNG for swap
src/vdl-theme-storage.js 32 Mutable window[INSTALL_FLAG] fallback allows re-installation
src/components/VdlModelEvalUI.vue 237 Hardcoded error color #c92a2a instead of CSS variable
src/components/VdlModelEvalUI.vue 200 External link missing noreferrer in rel attribute
tests/unit/home-quotes.spec.ts 119 Hardcoded catalog size (43) breaks on quote additions
tests/unit/model-eval.spec.ts 70 Hardcoded version assertion '0.0.1' breaks on every version bump
Files Reviewed (41 files)
  • ai-chat.js - 3 issues (model catalog restructuring, sanitization, dispose)
  • model-eval.js - 3 issues (scorers, concurrency planner, report rendering)
  • src/App.vue - reviewed (home quotes, tools nav, routing)
  • src/components/VdlAiChatUI.vue - reviewed (select chevron fix)
  • src/components/VdlModelEvalUI.vue - 3 issues (new eval UI component)
  • src/demos/ai-chat-demo.js - reviewed (font change)
  • src/demos/model-eval-harness.js - 1 issue (new browser harness)
  • src/demos/neptune-demo.js - reviewed (font change)
  • src/main.js - reviewed (theme storage install, theme defaults)
  • src/styles/labs.css - reviewed (theme customizer, quotes, tools route)
  • src/vdl-home-quotes.js - 4 issues (new shuffle-bag home quotes module)
  • src/vdl-theme-defaults.js - reviewed (new theme defaults)
  • src/vdl-theme-storage.js - 1 issue (new localStorage remap)
  • tests/unit/guardrails.spec.ts - reviewed (model ID updates)
  • tests/unit/home-quotes.spec.ts - 1 issue (new tests)
  • tests/unit/model-eval.spec.ts - 1 issue (new tests)
  • tests/unit/theme-storage.spec.ts - reviewed (new tests)
  • doc/vdl-ai-chat.md - reviewed (doc updates)
  • doc/vdl-model-eval.md - reviewed (new docs)
  • README.md - reviewed (version table, new component)
  • demo/model-eval-harness.html - reviewed (harness HTML)
  • package.json - reviewed (engines field)
  • pnpm-lock.yaml - skipped (generated)
  • vite.config.js - reviewed (model-eval copy, headers)
  • utils/fetch-ai-models.mjs - reviewed (new model catalog entries)
  • utils/model-eval-runner.mjs - 1 issue (new Chromium runner)
  • utils/model-eval-suite.json - reviewed (eval test cases)
  • openspec/ files (16 files) - skipped (planning docs)
  • data/model-eval-reports/latest/ (2 files) - skipped (generated artifacts)

Reviewed by deepseek-v4-pro · Input: 41.4K · Output: 15K · Cached: 331.1K

…hromium flag.

Unclosed think blocks no longer wipe trailing stream text via `$`-to-EOF;
ensureHomeQuoteBag uses randomSalt(); note --disable-web-security is local-eval only.

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

Copy link
Copy Markdown
Member Author

Addressed Kilo review P0/P1 from triage:

Fixed

  • Think-tag sanitizer: removed $-to-EOF so unclosed <think> / <|think|> no longer wipe streaming suffixes; orphan HTML think tags stripped; unit coverage added
  • ensureHomeQuoteBag: 'test-salt'randomSalt()
  • Documented --disable-web-security as local WebGPU/model-eval only (isolated profile)

Deferred / wontfix (per triage): W1–W3/W5/W6 and remaining suggestions (null guards, || vs ??, CSS polish, brittle test constants, etc.)

@Nostromo-618
Nostromo-618 merged commit f2f6baa into main Aug 8, 2026
2 checks passed
Comment thread ai-chat.js
out = out.replace(/<\|think\|>[\s\S]*?<\|\/think\|>/gi, '');
out = out.replace(/<think>[\s\S]*?<\/think>/gi, '');
// Orphan open/close markers left mid-stream (no closed pair yet).
out = out.replace(/<\|think\|>/g, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Missing i flag on <|think|> orphan regex — inconsistent with other case-insensitive think matches

All other think-related regexes on lines 326–327 and 330 use gi (case-insensitive). Line 329 uses only /g. If a model outputs <|THINK|> as an orphan token, the closed-block regex at line 326 would handle pairs, but this standalone strip would miss the case-variant, leaving a bare <|THINK|> in the output.

Suggested change
out = out.replace(/<\|think\|>/g, '');
out = out.replace(/<\|think\|>/gi, '');

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant