diff --git a/CHANGELOG.md b/CHANGELOG.md index dae2a81..4798f0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/); until 1.0, minor versions may contain breaking changes. +## [Unreleased] + +### Added + +- **Markdown in your messages:** your messages are shown formatted like the model's answers + (headings, lists, tables, code blocks, quotes, bold/italic); single line breaks stay as typed. + The eye button next to the text field shows a preview of the formatted message before sending. + ## [v202611-00016] – 2026-11-10 ### Fixed diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx index 0bda1f0..ba82a48 100644 --- a/web/src/components/ChatView.tsx +++ b/web/src/components/ChatView.tsx @@ -608,9 +608,10 @@ function UserText({ text }: { text: string }) { const last = rest.pop()!; if (last.trim().endsWith(":")) break; } + const body = rest.join("\n").trim(); return ( <> - {rest.join("\n").trimEnd()} + {body && } {files.length > 0 && (
{files.map((f) => ( diff --git a/web/src/components/Composer.tsx b/web/src/components/Composer.tsx index d77a77e..1b1645e 100644 --- a/web/src/components/Composer.tsx +++ b/web/src/components/Composer.tsx @@ -9,6 +9,7 @@ import type { ImageData, Session } from "../types"; import { ModelSelect } from "./ModelSelect"; import { EffortSelect } from "./Regenerate"; import { VoiceButton } from "./Voice"; +import { Markdown } from "./Markdown"; import { confirmDialog, promptDialog, toast } from "../dialogs"; interface Suggestion { @@ -67,6 +68,8 @@ export function Composer({ const [effort, setEffort] = useState(""); const [interim, setInterim] = useState(""); const [picker, setPicker] = useState(false); + // Rendered Markdown instead of the text field (to check formatting before sending). + const [preview, setPreview] = useState(false); const models = useModels(); const [prov, ...mid] = session.model.split("/"); const reasoning = !!models.find((p) => p.provider === prov)?.models.find((m) => m.id === mid.join("/"))?.reasoning; @@ -265,7 +268,20 @@ export function Composer({ ))}
)} + {preview && ( +
(setPreview(false), setTimeout(() => ref.current?.focus(), 0))} + onKeyDown={(e) => e.key === "Enter" && (setPreview(false), setTimeout(() => ref.current?.focus(), 0))} + > + {text.trim() ? : {t("composer.previewEmpty")}} +
+ )}