ui: UI/chat form follow ups (#26743)

* ui: split the markdown rendering setting per surface

User content and thinking get their own toggle again, so turning off
markdown for a message leaves reasoning blocks formatted. Both default
to markdown. A stored renderContentAsRawText unfolds onto the user key
and is dropped from the config.

File mentions render as badges in the raw text path too, through a
narrow pass over [name](file://path) that leaves everything else
untouched.

* ui: let the rich chat input scroll past its max height

The contenteditable renderer caps its height with max-height but had no
overflow rule, so a long buffer overflowed into the input area wrapper
and got clipped by its overflow-hidden, leaving no way to reach the
bottom of the message. The textarea renderer scrolls natively and was
never affected.

* ui: apply the new lint and format config

* ui: move the render keys unfolding into the migration service

Address review from @allozaur: the settings store no longer rewrites
persisted config on load, the raw text toggle now unfolds onto the
per-surface render keys in migration.service.ts, next to the other
config migrations. The mention scanner flag and the directory path
suffix become named constants.
This commit is contained in:
Pascal
2026-08-10 13:32:51 +02:00
committed by GitHub
parent e5275f6f77
commit 4dee52f82d
18 changed files with 282 additions and 109 deletions
@@ -0,0 +1,64 @@
// Guards the unfolding of `renderContentAsRawText` back onto the two
// per-surface render keys. The single toggle carried user content and
// thinking at once, so only the user key is restored from it and thinking
// returns to its own default. The toggle is removed from the persisted
// config so it does not stay orphaned in localStorage.
import { CONFIG_LOCALSTORAGE_KEY } from '$lib/constants/storage';
import { MigrationService } from '$lib/services/migration.service';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { beforeEach, describe, expect, it } from 'vitest';
const RENDER_KEYS_MIGRATION_ID = 'render-keys-unfold-v1';
async function seedConfig(stored: Record<string, unknown>) {
localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(stored));
const migration = MigrationService.getMigrations().find((m) => m.id === RENDER_KEYS_MIGRATION_ID);
await migration?.run();
settingsStore.initialize();
}
function persisted(): Record<string, unknown> {
return JSON.parse(localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}');
}
describe('renderContentAsRawText unfolding', () => {
beforeEach(() => {
localStorage.removeItem(CONFIG_LOCALSTORAGE_KEY);
MigrationService.resetState();
settingsStore.initialize();
});
it('maps raw text to user content as plain text', async () => {
await seedConfig({ renderContentAsRawText: true });
expect(config().renderUserContentAsMarkdown).toBe(false);
});
it('maps markdown to user content as markdown', async () => {
await seedConfig({ renderContentAsRawText: false });
expect(config().renderUserContentAsMarkdown).toBe(true);
});
it('leaves thinking on its own default', async () => {
await seedConfig({ renderContentAsRawText: true });
expect(config().renderThinkingAsMarkdown).toBe(true);
});
it('keeps an explicit user preference over the toggle', async () => {
await seedConfig({ renderContentAsRawText: true, renderUserContentAsMarkdown: true });
expect(config().renderUserContentAsMarkdown).toBe(true);
});
it('drops the toggle from the persisted config', async () => {
await seedConfig({ renderContentAsRawText: true });
expect(persisted().renderContentAsRawText).toBeUndefined();
});
it('leaves both surfaces on markdown when nothing is stored', async () => {
await seedConfig({});
expect(config().renderUserContentAsMarkdown).toBe(true);
expect(config().renderThinkingAsMarkdown).toBe(true);
});
});