Skip to content

Commit 07e9dbc

Browse files
Preview very large Markdown responses before rendering
1 parent 3dc288e commit 07e9dbc

7 files changed

Lines changed: 95 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 0.20.0 — native development (unreleased)
44

5+
- The desktop previews a very large Markdown response before rendering all of
6+
it, with an explicit control to expand the complete durable response.
7+
58
- Desktop windows can attach to an existing headless/TUI engine with independent
69
navigation and a visible shared-lifetime notice. Closing an attached window
710
leaves engine-owned work running. A bounded private event feed supports

‎docs/NATIVE_HISTORY.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ These are saved-history/initial-load bounds, not a claim of constant memory for
3030
long-running live task or a virtualized Markdown renderer. Further live-stream,
3131
large-single-message and WebKit memory stress remain native release gates.
3232

33+
## Large response previews
34+
35+
The desktop renders the first 512 KiB of a single Markdown response by default.
36+
For a larger response, a visible **Show full response** control expands the
37+
complete text in place. This avoids parsing a provider's maximum 16 MiB response
38+
into the WebKit DOM while it is still streaming. It does not alter the event,
39+
continuation context, task export or JSON export; the full saved text remains
40+
available through those paths. The preview is measured in JavaScript UTF-16 code
41+
units, so the displayed byte size is approximate for non-ASCII text.
42+
43+
This is an initial rendering bound, not transcript virtualization. Expanding a
44+
large response deliberately accepts the cost of rendering it. A regression test
45+
verifies that the Markdown parser receives only the preview until the reader
46+
chooses to expand it.
47+
3348
## Live rendering
3449

3550
Unchanged Markdown messages reuse their rendered output during live updates.

‎docs/NATIVE_MIGRATION.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ Implemented foundation:
144144
retain valid function-call histories; interrupted calls are never blindly replayed.
145145
- [Paged native desktop history](NATIVE_HISTORY.md) bounds saved snapshots and
146146
supports older/newer/latest navigation while current work continues. Original
147-
records and exports remain intact. Long-running live transcript memory and
148-
renderer virtualization remain separate gates.
147+
records and exports remain intact. Large single responses preview the first
148+
512 KiB before an explicit full render. Long-running live transcript memory
149+
and renderer virtualization remain separate gates.
149150
- A [desktop follow-up queue](NATIVE_QUEUE.md) with visible project-wide waiting
150151
messages, model/mode snapshots, independent queued cancellation, continued
151152
streaming, conversation switching and reload recovery. Active history remains

‎docs/NATIVE_VERIFICATION.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,3 +1014,19 @@ lifetime, shutdown, accessibility and history checks still pass. The final
10141014
abort-on-drop guard was verified by the focused cancellation tests after this
10151015
window run. These checks do not assert that an OS notification daemon displayed
10161016
a popup; that integration gate remains separate.
1017+
1018+
## Large Markdown response preview
1019+
1020+
The desktop now defers parsing response text above 512 KiB until the reader
1021+
chooses **Show full response**. The durable event, continuation context and JSON
1022+
export retain the full provider response, including responses within the native
1023+
engine's 16 MiB response limit. The UI regression creates a response larger than
1024+
the preview boundary, confirms that React Markdown receives exactly the bounded
1025+
prefix, then expands it and verifies the original full string is parsed. The
1026+
complete UI suite passes 35 tests, along with all seven browser scenarios and a
1027+
rebuilt native desktop binary. The real WebKit window suite also passes its full
1028+
30-model-request workflow after the embedded-interface rebuild.
1029+
1030+
This does not claim transcript virtualization or a fixed memory bound after the
1031+
reader expands a large response. Those longer-duration live-transcript and
1032+
WebKit-memory gates remain separate.

‎ui/src/components/Markdown.test.tsx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
screen,
77
waitFor,
88
} from "@testing-library/react";
9-
import { Markdown } from "./Markdown";
9+
import { Markdown, MARKDOWN_PREVIEW_LIMIT } from "./Markdown";
1010

1111
const parses = vi.hoisted(() => vi.fn());
1212
vi.mock("react-markdown", async (original) => {
@@ -60,3 +60,20 @@ it("parses only the changing response while earlier Markdown stays interactive",
6060
view.rerender(<Conversation response="Streaming **99**" />);
6161
expect(parses).toHaveBeenCalledTimes(102);
6262
});
63+
64+
it("bounds a very large response until the reader explicitly expands it", () => {
65+
const large = `# Large response\n\n${"x".repeat(MARKDOWN_PREVIEW_LIMIT + 1024)}`;
66+
render(<Markdown>{large}</Markdown>);
67+
expect(parses).toHaveBeenCalledTimes(1);
68+
expect(String(parses.mock.calls[0][0])).toHaveLength(MARKDOWN_PREVIEW_LIMIT);
69+
expect(screen.getByRole("status").textContent).toContain("512 KiB");
70+
expect(
71+
screen.getByRole("button", { name: "Show full response" }),
72+
).toBeTruthy();
73+
fireEvent.click(screen.getByRole("button", { name: "Show full response" }));
74+
expect(parses).toHaveBeenCalledTimes(2);
75+
expect(parses.mock.calls[1][0]).toBe(large);
76+
expect(
77+
screen.queryByRole("button", { name: "Show full response" }),
78+
).toBeNull();
79+
});

‎ui/src/components/Markdown.tsx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import ReactMarkdown from "react-markdown";
33
import remarkGfm from "remark-gfm";
44
import { Check, Copy } from "lucide-react";
55

6+
// Providers may send a response up to 16 MiB. Rendering all of that as one
7+
// Markdown tree can make the desktop unresponsive, while the durable event and
8+
// JSON export still retain the complete response.
9+
export const MARKDOWN_PREVIEW_LIMIT = 512 * 1024;
10+
611
function CodeBlock({ children, ...props }: ComponentPropsWithoutRef<"pre">) {
712
const [copied, setCopied] = useState(false);
813
return (
@@ -38,6 +43,11 @@ export const Markdown = memo(function Markdown({
3843
}: {
3944
children: string;
4045
}) {
46+
const [expanded, setExpanded] = useState(false);
47+
const abbreviated = children.length > MARKDOWN_PREVIEW_LIMIT && !expanded;
48+
const visible = abbreviated
49+
? children.slice(0, MARKDOWN_PREVIEW_LIMIT)
50+
: children;
4151
return (
4252
<div className="markdown">
4353
<ReactMarkdown
@@ -55,8 +65,23 @@ export const Markdown = memo(function Markdown({
5565
),
5666
}}
5767
>
58-
{children}
68+
{visible}
5969
</ReactMarkdown>
70+
{abbreviated && (
71+
<div className="markdown-preview" role="status">
72+
<p className="hint">
73+
Showing the first 512 KiB of this response. The complete response is
74+
retained in task history and JSON export.
75+
</p>
76+
<button
77+
type="button"
78+
className="mini"
79+
onClick={() => setExpanded(true)}
80+
>
81+
Show full response
82+
</button>
83+
</div>
84+
)}
6085
</div>
6186
);
6287
});

‎ui/src/workspace.css‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,20 @@ kbd {
587587
.markdown {
588588
overflow-wrap: anywhere;
589589
}
590+
.markdown-preview {
591+
display: flex;
592+
align-items: center;
593+
justify-content: space-between;
594+
gap: 12px;
595+
margin: 14px 0;
596+
padding: 10px 12px;
597+
border: 1px solid var(--border);
598+
border-radius: 10px;
599+
background: var(--surface-2);
600+
}
601+
.markdown-preview p {
602+
margin: 0;
603+
}
590604
.markdown > :first-child {
591605
margin-top: 0;
592606
}

0 commit comments

Comments
 (0)