Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
de70f40
feat(web): fenced code blocks in the rich text composer
chrisdeeming Sep 17, 2026
a6a933e
feat(web): toggle the composer between rich text and Markdown source
chrisdeeming Sep 17, 2026
a12e351
feat(web): rendered lists in the rich text composer
chrisdeeming Sep 17, 2026
d5689a0
feat(web): block quotes in the rich text composer
chrisdeeming Sep 17, 2026
c0c998e
feat(web): horizontal rules in the rich text composer
chrisdeeming Sep 17, 2026
ba4d806
feat(web): headings in the rich text composer
chrisdeeming Sep 17, 2026
337fd3f
feat(web): type GFM task items whole in the rich text composer
chrisdeeming Sep 18, 2026
0beed60
chore(web): drop unused composer exports
chrisdeeming Sep 18, 2026
f944a85
fix(web): keep fence editing inside one code block
chrisdeeming Sep 18, 2026
53f2044
fix(web): keep fence info strings and bare list markers round-tripping
chrisdeeming Sep 18, 2026
ba4bd26
fix(web): render composer ordered markers from their source
chrisdeeming Sep 18, 2026
460d070
fix(web): accept any whitespace after a typed dash marker
chrisdeeming Sep 18, 2026
3107fc9
refactor(web): share the code block frame between chat and the composer
chrisdeeming Sep 18, 2026
bf30636
docs(user): lint-clean code spans in the composer guide
chrisdeeming Sep 18, 2026
837c09d
fix(web): only open fences at a top-level paragraph
chrisdeeming Sep 18, 2026
bcf7894
fix(web): let a selection leaving a fence reach the composer's Tab an…
chrisdeeming Sep 18, 2026
a8a4ab8
fix(web): keep the whitespace typed after quote, heading and rule mar…
chrisdeeming Sep 18, 2026
6045eec
fix(web): keep the composer's inline code styling off fenced code
chrisdeeming Sep 18, 2026
9a2e2b3
refactor(web): compose the composer fence from chat's code block frame
chrisdeeming Sep 18, 2026
3da3067
fix(web): move the source toggle beside the attach button
chrisdeeming Sep 19, 2026
be71769
refactor(web): build the source toggle on the shared Toggle primitive
chrisdeeming Sep 19, 2026
d966b45
feat(web): toggle rich text from the composer with mod+/
chrisdeeming Sep 21, 2026
97eb21e
chore(web): format package.json after the rebase resolution
chrisdeeming Sep 21, 2026
a0d433b
fix(web): one press of the rich text shortcut is one flip
chrisdeeming Sep 21, 2026
f040557
fix(web): trim the fence info string before highlighting
chrisdeeming Sep 21, 2026
e0d3e9f
fix(web): convert any bullet to a task with [ ]
chrisdeeming Sep 21, 2026
ae3deb4
fix(web): let code block frame callers add header classes
chrisdeeming Sep 21, 2026
60acf8c
fix(web): order Usage keybinding rows the same for any input order
chrisdeeming Sep 28, 2026
709ee8d
fix(web): style the composer fence body from theme values
chrisdeeming Sep 28, 2026
6c33c25
test(web): lock in that splitting a list item keeps its source marker
chrisdeeming Sep 28, 2026
0df805d
feat(web): choose a fence's language from its header
chrisdeeming Sep 28, 2026
ecaaae7
fix(web): align the fence language picker and give every language an …
chrisdeeming Sep 28, 2026
6a2260b
fix(web): keep a fence's leading whitespace when changing its language
chrisdeeming Sep 28, 2026
3b2c978
chore: re-run CI
chrisdeeming Sep 28, 2026
31bef4b
fix(web): drop Tiptap's block shortcuts from the composer
chrisdeeming Sep 28, 2026
8a5b6fe
perf(web): leave very large fences unhighlighted
chrisdeeming Sep 28, 2026
a3a952a
docs: cut the composer docs to what readers need
chrisdeeming Sep 28, 2026
08ea89f
fix(web): keep Shift+Enter inside a fence as a newline
chrisdeeming Sep 28, 2026
16de7fa
fix(web): group composer undo the way the Lexical composer did
chrisdeeming Sep 28, 2026
be2d989
fix(web): make a paste its own undo step in the real composer
chrisdeeming Sep 28, 2026
bd1c399
Merge branch 'main' into feat/composer-code-blocks
chrisdeeming Oct 1, 2026
3eb02d3
test(web): drop the duplicate usage.open left by the main merge
chrisdeeming Oct 1, 2026
e8e3a52
fix(web): keep every fence highlighted and align CRLF tokens
chrisdeeming Oct 1, 2026
15c77ab
fix(web): drop highlights that resolve after their fence changed
chrisdeeming Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
"@tanstack/react-pacer": "^0.19.4",
"@tanstack/react-router": "^1.160.2",
"@tiptap/core": "^3.31.3",
"@tiptap/extension-blockquote": "^3.31.3",
"@tiptap/extension-code": "^3.31.3",
"@tiptap/extension-code-block": "^3.31.3",
"@tiptap/extension-heading": "^3.31.3",
"@tiptap/extension-horizontal-rule": "^3.31.3",
"@tiptap/extension-list": "^3.31.3",
"@tiptap/extension-task-item": "^3.31.3",
"@tiptap/extension-task-list": "^3.31.3",
"@tiptap/pm": "^3.31.3",
Expand Down
81 changes: 66 additions & 15 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,63 @@ function MarkdownDetails({
* icon (redundant next to its own name) and fall back to the language text
* when no specific icon exists or it fails to load.
*/
/**
* The code block frame, shared with the composer so an editable fence and a
* rendered one cannot drift apart: the wrapper, its header row with the
* language title, an optional actions slot, and the body as children. The
* composer passes Tiptap's node view wrapper as the outer element.
*/
export function MarkdownCodeBlockFrame({
as: Wrapper = "div",
language,
fenceTitle,
theme,
wrapped = true,
title,
actions,
headerProps,
children,
}: {
as?: React.ElementType;
language: string;
fenceTitle: string | null;
theme: "light" | "dark";
wrapped?: boolean;
/** Replaces the language title, as the composer does with its language picker. */
title?: React.ReactNode;
actions?: React.ReactNode;
headerProps?: React.HTMLAttributes<HTMLDivElement>;
children: React.ReactNode;
}) {
return (
<Wrapper
className="chat-markdown-codeblock my-[0.65rem] overflow-hidden rounded-lg border border-border/70 bg-secondary leading-snug dark:border-transparent dark:bg-input/32"
data-language={language}
data-wrap={wrapped ? "true" : "false"}
>
<div
{...headerProps}
className={cn(
"chat-markdown-codeblock-header flex items-center justify-between gap-2 pt-1.5 pr-1.5 pb-0 pl-3 select-none",
headerProps?.className,
)}
>
{title ?? (
<span className="inline-flex min-w-0 items-center gap-1.5 font-mono text-2xs">
<MarkdownCodeBlockTitleContent
fenceTitle={fenceTitle}
language={language}
theme={theme}
/>
</span>
)}
{actions}
</div>
{children}
</Wrapper>
);
}

function MarkdownCodeBlockTitleContent({
fenceTitle,
language,
Expand Down Expand Up @@ -1007,19 +1064,12 @@ function MarkdownCodeBlock({
);

return (
<div
className="chat-markdown-codeblock my-[0.65rem] overflow-hidden rounded-lg border border-border/70 bg-secondary leading-snug dark:border-transparent dark:bg-input/32"
data-language={language}
data-wrap={wrapped ? "true" : "false"}
>
<div className="chat-markdown-codeblock-header flex items-center justify-between gap-2 pt-1.5 pr-1.5 pb-0 pl-3 select-none">
<span className="inline-flex min-w-0 items-center gap-1.5 font-mono text-2xs">
<MarkdownCodeBlockTitleContent
fenceTitle={fenceTitle}
language={language}
theme={theme}
/>
</span>
<MarkdownCodeBlockFrame
language={language}
fenceTitle={fenceTitle}
theme={theme}
wrapped={wrapped}
actions={
<span className="flex items-center gap-0.5" role="toolbar" aria-label="Code block actions">
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -1073,9 +1123,10 @@ function MarkdownCodeBlock({
<TooltipPopup side="top">{copyLabel}</TooltipPopup>
</Tooltip>
</span>
</div>
}
>
{children}
</div>
</MarkdownCodeBlockFrame>
);
}

Expand Down
Loading
Loading