feat: message format, pretty markdown

This commit is contained in:
Shivering Ocelot committed 2026-10-10 22:55:10 -05:00
1 parent 3d8f77d154
commit 5c7a106413
7 files changed
+95 -15

No files matched your search

+1
View File
@@ -13,6 +13,7 @@ def message_json(msg):
"sender": msg.sender, "sender": msg.sender,
"kind": msg.kind, "kind": msg.kind,
"subkind": msg.subkind, "subkind": msg.subkind,
"format": msg.format,
"content": msg.content, "content": msg.content,
"threadId": msg.thread_id, "threadId": msg.thread_id,
} }
+1 -1
View File
@@ -276,7 +276,7 @@ def register(server, app, auth_manager):
elif action == "update-chat-message": elif action == "update-chat-message":
message = await app.update_chat_message( message = await app.update_chat_message(
message_id=body['messageId'], message_id=body['messageId'],
content=body['content'], content=body.get('content'),
kind=body.get('kind'), kind=body.get('kind'),
subkind=body.get('subkind'), subkind=body.get('subkind'),
format=body.get('format'), format=body.get('format'),
+10
View File
@@ -95,3 +95,13 @@ input:disabled, textarea:disabled, select:disabled {
.with-sidebar-expanded .when-sidebar-collapsed { .with-sidebar-expanded .when-sidebar-collapsed {
display: none; display: none;
} }
/* Content */
h1, h2, h3, h4, h5, h6 {
margin: 1em 0 0.5em;
font-weight: bold;
}
p, ul, il {
margin: 0.5em 0;
}
+27
View File
@@ -545,6 +545,33 @@ export class ChatSession {
this.creatingThread.value = Boolean(fork); this.creatingThread.value = Boolean(fork);
} }
// Sets the render format of a message ("markdown", or "" for plain text).
// Omit `content` so the backend leaves the existing content untouched.
setMessageFormat (message, format) {
backend.sendAction("update-chat-message", {
messageId: message.id, format,
}).then(response => {
if (response && response.error) {
ui.alert("Error updating message format: " + response.error);
}
});
// No update event is broadcast (matching the content-edit path above), so
// patch the source message and rebuild the entries locally. Other sessions
// and stale views won't reflect this until reload — which is correct, since
// the format is persisted. If we ever broadcast edits, flip this to rely on
// the incoming update instead.
let thread = this.getThread(message.threadId);
let existing = thread.messages.find(msg => msg.id == message.id);
if (existing) {
existing.format = format;
} else {
console.error("Can't set format locally?", {messageId: message.id, thread});
}
this.computeChatEntries();
}
// Adds a bulk of messages to the message list, in their correct // Adds a bulk of messages to the message list, in their correct
// chronological position and replacing duplicates // chronological position and replacing duplicates
addMessageBulk (messages) { addMessageBulk (messages) {
+43 -13
View File
@@ -4,13 +4,17 @@ import { marked } from "marked";
import * as backend from '../backend'; import * as backend from '../backend';
marked.use({ marked.use({
renderer: { renderer: {
code (code, infostring, escaped) { code ({lang, text}) {
let langElem = infostring ? let langElem = lang ?
`<div class="language">${infostring}</div>` : ""; `<div class="code-lang">${lang}</div>` : "";
const div = document.createElement('div');
div.textContent = text;
const escaped = div.innerHTML;
return `<div class="code"> return `<div class="code">
${langElem} ${langElem}
<div class="content">${code}</div> <div class="code-content">${escaped}</div>
</div>`; </div>`;
} }
} }
@@ -111,7 +115,7 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
<template v-if="message.type == 'message' || message.type == null"> <template v-if="message.type == 'message' || message.type == null">
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div> <div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div> <div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="md-content" style="white-space: collapse;"></div>
<div v-else class="content">{{content}}</div> <div v-else class="content">{{content}}</div>
</template> </template>
@@ -189,14 +193,6 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
margin-right: 2em; margin-right: 2em;
} }
.message .content {
white-space: preserve;
}
.message ul, .message ol {
white-space: normal;
}
.message .sender, .message .sender,
.note .sender { .note .sender {
font-weight: bold; font-weight: bold;
@@ -288,4 +284,38 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
font-style: normal; font-style: normal;
font-weight: bold; font-weight: bold;
} }
.md-content {
white-space: collapse;
width: fit-content;
max-width: 50em;
line-height: normal;
word-wrap: anywhere;
}
</style>
<style>
/* Code blocks are generated at runtime, these styles cannot be scoped */
.code {
background-color: var(--color-surface-input);
border-radius: var(--border-radius);
margin: .5em 0;
padding: .5em;
}
.code-lang {
padding-bottom: .5em;
margin-top: -.5em;
font-weight: bold;
user-select: none;
}
.code-content {
white-space: pre;
font-family: monospace;
overflow-x: auto;
line-height: 1.6rem;
}
</style> </style>
+1 -1
View File
@@ -23,8 +23,8 @@
<style scoped> <style scoped>
:slotted(h1) { :slotted(h1) {
font-size: 160%; font-size: 160%;
font-weight: bold;
padding: 0 0.4em; padding: 0 0.4em;
margin: 0;
} }
.main-header { .main-header {
+12
View File
@@ -130,6 +130,12 @@
{value: 'pin', title: "Pin message"}, {value: 'pin', title: "Pin message"},
{value: 'edit', title: "Edit in-place"}, {value: 'edit', title: "Edit in-place"},
{value: 'edit-fork', title: "Edit in new thread"}, {value: 'edit-fork', title: "Edit in new thread"},
// ContextMenu.vue uses null for "dismissed", so the "None" entry can't
// carry a null value; "format:" (empty suffix) is the sentinel for "".
{title: "Set format", entries: [
{value: 'format:markdown', title: "Markdown"},
{value: 'format:', title: "None"},
]},
]; ];
// Long logs can be expanded/collapsed // Long logs can be expanded/collapsed
@@ -164,6 +170,12 @@
} }
break; break;
} }
default: {
if (selected?.startsWith('format:')) {
session.setMessageFormat(message, selected.slice('format:'.length));
}
break;
}
} }
} }