Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca14ba3338 | ||
|
|
97e66d2118 |
No files matched your search
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
|
||||||
until 1.0, minor versions may contain breaking changes.
|
until 1.0, minor versions may contain breaking changes.
|
||||||
|
|
||||||
|
## [v202611-00017] – 2026-11-10
|
||||||
|
|
||||||
|
### 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
|
## [v202611-00016] – 2026-11-10
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Version v202611-00016" src="https://img.shields.io/badge/version-v202611--00016-d69e2e?style=flat-square">
|
<img alt="Version v202611-00017" src="https://img.shields.io/badge/version-v202611--00017-d69e2e?style=flat-square">
|
||||||
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
<img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-689d6a?style=flat-square">
|
||||||
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
<img alt="Rust backend" src="https://img.shields.io/badge/backend-Rust-b7410e?style=flat-square&logo=rust">
|
||||||
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
<img alt="React PWA" src="https://img.shields.io/badge/frontend-React%20PWA-2d81ff?style=flat-square&logo=react">
|
||||||
|
|||||||
@@ -608,9 +608,10 @@ function UserText({ text }: { text: string }) {
|
|||||||
const last = rest.pop()!;
|
const last = rest.pop()!;
|
||||||
if (last.trim().endsWith(":")) break;
|
if (last.trim().endsWith(":")) break;
|
||||||
}
|
}
|
||||||
|
const body = rest.join("\n").trim();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rest.join("\n").trimEnd()}
|
{body && <Markdown text={body} breaks />}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="user-files">
|
<div className="user-files">
|
||||||
{files.map((f) => (
|
{files.map((f) => (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { ImageData, Session } from "../types";
|
|||||||
import { ModelSelect } from "./ModelSelect";
|
import { ModelSelect } from "./ModelSelect";
|
||||||
import { EffortSelect } from "./Regenerate";
|
import { EffortSelect } from "./Regenerate";
|
||||||
import { VoiceButton } from "./Voice";
|
import { VoiceButton } from "./Voice";
|
||||||
|
import { Markdown } from "./Markdown";
|
||||||
import { confirmDialog, promptDialog, toast } from "../dialogs";
|
import { confirmDialog, promptDialog, toast } from "../dialogs";
|
||||||
|
|
||||||
interface Suggestion {
|
interface Suggestion {
|
||||||
@@ -67,6 +68,8 @@ export function Composer({
|
|||||||
const [effort, setEffort] = useState("");
|
const [effort, setEffort] = useState("");
|
||||||
const [interim, setInterim] = useState("");
|
const [interim, setInterim] = useState("");
|
||||||
const [picker, setPicker] = useState(false);
|
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 models = useModels();
|
||||||
const [prov, ...mid] = session.model.split("/");
|
const [prov, ...mid] = session.model.split("/");
|
||||||
const reasoning = !!models.find((p) => p.provider === prov)?.models.find((m) => m.id === mid.join("/"))?.reasoning;
|
const reasoning = !!models.find((p) => p.provider === prov)?.models.find((m) => m.id === mid.join("/"))?.reasoning;
|
||||||
@@ -265,7 +268,20 @@ export function Composer({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{preview && (
|
||||||
|
<div
|
||||||
|
className="composer-preview"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
title={t("composer.previewEdit")}
|
||||||
|
onClick={() => (setPreview(false), setTimeout(() => ref.current?.focus(), 0))}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && (setPreview(false), setTimeout(() => ref.current?.focus(), 0))}
|
||||||
|
>
|
||||||
|
{text.trim() ? <Markdown text={text} breaks /> : <span className="hint">{t("composer.previewEmpty")}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<textarea
|
<textarea
|
||||||
|
style={preview ? { display: "none" } : undefined}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
rows={1}
|
rows={1}
|
||||||
value={text}
|
value={text}
|
||||||
@@ -337,6 +353,15 @@ export function Composer({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
className={`ghost ${preview ? "active" : ""}`}
|
||||||
|
style={{ padding: "3px 6px" }}
|
||||||
|
onClick={() => setPreview(!preview)}
|
||||||
|
aria-pressed={preview}
|
||||||
|
title={t(preview ? "composer.previewEdit" : "composer.preview")}
|
||||||
|
>
|
||||||
|
<Icon name={preview ? "edit" : "eye"} />
|
||||||
|
</button>
|
||||||
<VoiceButton onText={(d) => setText(text ? `${text.replace(/\s+$/, "")} ${d}` : d)} onInterim={setInterim} />
|
<VoiceButton onText={(d) => setText(text ? `${text.replace(/\s+$/, "")} ${d}` : d)} onInterim={setInterim} />
|
||||||
<label className="ghost" style={{ cursor: "pointer", padding: "3px 6px" }} title={t("composer.attach")}>
|
<label className="ghost" style={{ cursor: "pointer", padding: "3px 6px" }} title={t("composer.attach")}>
|
||||||
<Icon name="clip" />
|
<Icon name="clip" />
|
||||||
|
|||||||
@@ -44,7 +44,30 @@ export function SvgBlock({ code, children }: { code: string; children?: ReactNod
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Markdown = memo(function Markdown({ text, baseDir }: { text: string; baseDir?: string }) {
|
/**
|
||||||
|
* Keep single line breaks as typed (Markdown would join the lines into one paragraph):
|
||||||
|
* a hard break at the end of every line that continues a paragraph. Code blocks stay as
|
||||||
|
* they are.
|
||||||
|
*/
|
||||||
|
export function keepLineBreaks(text: string) {
|
||||||
|
let fence: string | null = null;
|
||||||
|
const lines = text.split("\n");
|
||||||
|
return lines
|
||||||
|
.map((line, i) => {
|
||||||
|
const f = line.match(/^\s*(`{3,}|~{3,})/);
|
||||||
|
if (f) {
|
||||||
|
if (!fence) fence = f[1][0];
|
||||||
|
else if (f[1][0] === fence) fence = null;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
const next = lines[i + 1];
|
||||||
|
if (fence || next === undefined || !line.trim() || !next.trim() || / {2}$|\\$/.test(line)) return line;
|
||||||
|
return `${line} `;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Markdown = memo(function Markdown({ text, baseDir, breaks }: { text: string; baseDir?: string; breaks?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="markdown">
|
<div className="markdown">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@@ -81,7 +104,7 @@ export const Markdown = memo(function Markdown({ text, baseDir }: { text: string
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{breaks ? keepLineBreaks(text) : text}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -663,6 +663,9 @@ export const de: Messages = {
|
|||||||
"composer.mode": "Modus",
|
"composer.mode": "Modus",
|
||||||
"composer.model": "Modell",
|
"composer.model": "Modell",
|
||||||
"composer.permissionMode": "Berechtigungsmodus",
|
"composer.permissionMode": "Berechtigungsmodus",
|
||||||
|
"composer.preview": "Vorschau (Markdown)",
|
||||||
|
"composer.previewEdit": "Zurück zum Bearbeiten",
|
||||||
|
"composer.previewEmpty": "Noch nichts zum Anzeigen.",
|
||||||
"composer.attach": "Dateien anhängen",
|
"composer.attach": "Dateien anhängen",
|
||||||
|
|
||||||
"command.compact": "Unterhaltung zusammenfassen, um Kontext freizugeben",
|
"command.compact": "Unterhaltung zusammenfassen, um Kontext freizugeben",
|
||||||
|
|||||||
@@ -662,6 +662,9 @@ export const en = {
|
|||||||
"composer.mode": "Mode",
|
"composer.mode": "Mode",
|
||||||
"composer.model": "Model",
|
"composer.model": "Model",
|
||||||
"composer.permissionMode": "Permission mode",
|
"composer.permissionMode": "Permission mode",
|
||||||
|
"composer.preview": "Preview (Markdown)",
|
||||||
|
"composer.previewEdit": "Back to editing",
|
||||||
|
"composer.previewEmpty": "Nothing to preview yet.",
|
||||||
"composer.attach": "Attach files",
|
"composer.attach": "Attach files",
|
||||||
|
|
||||||
"command.compact": "Summarize the conversation to free up context",
|
"command.compact": "Summarize the conversation to free up context",
|
||||||
|
|||||||
@@ -527,6 +527,20 @@ button.session-group:hover {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
/* User messages are Markdown too; the line breaks come from keepLineBreaks. */
|
||||||
|
.msg-user .markdown {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.composer-preview {
|
||||||
|
min-height: 44px;
|
||||||
|
max-height: 40vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px dashed var(--bg3);
|
||||||
|
background: var(--bg1);
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
.msg-user .msg-actions {
|
.msg-user .msg-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
* under "unreleased"; scripts/release.sh renames the key to the release version.
|
||||||
*/
|
*/
|
||||||
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
export const WHATS_NEW: Record<string, { en: string[]; de: string[] }> = {
|
||||||
|
"202611-00017": {
|
||||||
|
en: ["Your messages are shown as Markdown; the eye button next to the text field previews it before sending"],
|
||||||
|
de: ["Deine Nachrichten werden als Markdown dargestellt; das Augen-Symbol neben dem Textfeld zeigt vor dem Senden eine Vorschau"],
|
||||||
|
},
|
||||||
"202611-00014": {
|
"202611-00014": {
|
||||||
en: ["Folder picker (new automation, new chat): create a new folder right there"],
|
en: ["Folder picker (new automation, new chat): create a new folder right there"],
|
||||||
de: ["Ordnerauswahl (neue Automatisierung, neuer Chat): direkt einen neuen Ordner anlegen"],
|
de: ["Ordnerauswahl (neue Automatisierung, neuer Chat): direkt einen neuen Ordner anlegen"],
|
||||||
|
|||||||
Reference in new issue
Block a user