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
@@ -797,7 +797,7 @@
data-placeholder={placeholder}
tabindex={disabled ? -1 : 0}
class={[
'chat-form-contenteditable text-md min-h-12 w-full whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
'chat-form-contenteditable text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
disabled && 'cursor-not-allowed'
]}
style="max-height: var(--max-message-height);"
@@ -164,7 +164,7 @@
? `max-height: ${MAX_HEIGHT}px;`
: 'max-height: none;'}
>
{#if !currentConfig.renderContentAsRawText}
{#if currentConfig.renderUserContentAsMarkdown}
<div bind:this={messageElement} class={isExpanded ? 'cursor-text' : ''}>
<MarkdownContent class="markdown-system-content" content={message.content} />
</div>
@@ -1,5 +1,5 @@
<script lang="ts">
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
import { ChatAttachmentsList, MarkdownContent, MentionText } from '$lib/components/app';
import { Card } from '$lib/components/ui/card';
import { config } from '$lib/stores/settings.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
@@ -64,14 +64,14 @@
data-multiline={isMultiline ? '' : undefined}
style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
>
{#if renderMarkdown && !currentConfig.renderContentAsRawText}
{#if renderMarkdown && currentConfig.renderUserContentAsMarkdown}
<div bind:this={messageElement}>
<MarkdownContent class="markdown-user-content" {content} />
</div>
{:else}
<span bind:this={messageElement} class="text-md whitespace-pre-wrap">
{content}
</span>
<span bind:this={messageElement} class="text-md whitespace-pre-wrap"
><MentionText {content} /></span
>
{/if}
</Card>
{/if}
@@ -140,7 +140,7 @@
class:is-streaming={isPending}
onscroll={handleScrollEvent}
>
{#if !currentConfig.renderContentAsRawText}
{#if currentConfig.renderThinkingAsMarkdown}
<MarkdownContent content={section.content} class="text-muted-foreground" {attachments} />
{:else}
<div
@@ -0,0 +1,36 @@
<script lang="ts">
import { SETTINGS_KEYS } from '$lib/constants';
import { settingsStore } from '$lib/stores/settings.svelte';
import { toolsStore } from '$lib/stores/tools.svelte';
import {
getMentionBadgeIconPaths,
getMentionBadgeLabel,
MENTION_BADGE_CLASSNAME,
MENTION_BADGE_ICON_CLASSNAME,
MENTION_BADGE_SVG_ATTRIBUTES
} from '$lib/utils';
interface Props {
name: string;
path: string;
}
let { name, path }: Props = $props();
let showFullPath = $derived(
settingsStore.getConfig(SETTINGS_KEYS.SHOW_FULL_PATH_IN_MENTIONS) as boolean
);
let label = $derived(getMentionBadgeLabel(name, path, showFullPath, toolsStore.serverHome));
</script>
<!-- The chip is a flex container, so template whitespace between its
children collapses away and the icon keeps its `gap-1` spacing. -->
<span class={MENTION_BADGE_CLASSNAME} title={path}>
<svg {...MENTION_BADGE_SVG_ATTRIBUTES} class={MENTION_BADGE_ICON_CLASSNAME}>
{#each getMentionBadgeIconPaths(path) as d (d)}
<path {d} />
{/each}
</svg>
<span class="shrink-0 truncate">{label}</span>
</span>
@@ -0,0 +1,17 @@
<script lang="ts">
import MentionBadge from './MentionBadge.svelte';
import { splitMentionSegments } from '$lib/utils';
interface Props {
content: string;
}
let { content }: Props = $props();
let segments = $derived(splitMentionSegments(content));
</script>
<!-- Segments sit in a `whitespace-pre-wrap` parent, so the markup stays
glued: any newline between the tags below would print as a space. -->
<!-- prettier-ignore -->
{#each segments as segment, index (index)}{#if segment.mention}<MentionBadge name={segment.mention.name} path={segment.mention.path} />{:else}{segment.text}{/if}{/each}
@@ -31,6 +31,20 @@
*/
export { default as MarkdownContent } from './MarkdownContent/MarkdownContent.svelte';
/**
* **MentionText** - Plain text with file mention badges
*
* Renders a message verbatim, turning only `[name](file://path)` links
* into the same badge chips the markdown path draws. Nothing else is
* interpreted, so pasted code keeps its `#` comments and underscores.
*
* @example
* ```svelte
* <span class="whitespace-pre-wrap"><MentionText content={message.content} /></span>
* ```
*/
export { default as MentionText } from './MentionText.svelte';
/**
* **SyntaxHighlightedCode** - Code syntax highlighting
*