Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1,705 changes: 1,705 additions & 0 deletions docs/plan/agent-execution/PRD-shell-execution.md

Large diffs are not rendered by default.

1,601 changes: 1,601 additions & 0 deletions docs/plan/agent-execution/PRD-subagent-control.md

Large diffs are not rendered by default.

673 changes: 673 additions & 0 deletions docs/plan/agent-execution/TYPOGRAPHY-PROPOSAL.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/chat-surface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"@0x-copilot/api-types": "0.1.0",
"@0x-copilot/chat-transport": "0.1.0",
"@0x-copilot/design-system": "0.1.0",
"@shikijs/langs": "4.4.2",
"shiki": "4.4.2",
"unified": "11.0.5",
"unist-util-visit": "5.1.0"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/chat-surface/src/messages/MarkdownText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useMemo, type ComponentProps, type ReactElement } from "react";
import { Streamdown, defaultRemarkPlugins } from "streamdown";

import { createRemarkCitations } from "./citationRemarkPlugin";
import { codeHighlighterPlugins } from "./highlight/codeHighlighterPlugin";
import { streamingCursorProps } from "./streamingCursor";
import type { TextMessagePartProps } from "./types";

Expand Down Expand Up @@ -54,10 +55,15 @@ export function MarkdownText({
// mode/isAnimating/animated triple. Any future streaming-text surface
// (tool output, subagent transcript, …) imports the same helper rather
// than re-deriving the toggle.
// `plugins.code` is streamdown's syntax-highlighting seam, and it ships
// nothing in it — an unfilled hole is why every fenced block in the app
// rendered as flat grey text. The constant is module-scope because
// streamdown memoises its plugin context on the object's identity.
return (
<Streamdown
{...streamingCursorProps(status)}
components={components}
plugins={codeHighlighterPlugins}
remarkPlugins={remarkPlugins}
>
{text}
Expand Down
2 changes: 2 additions & 0 deletions packages/chat-surface/src/messages/Reasoning.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ReactElement } from "react";
import { Streamdown } from "streamdown";

import { codeHighlighterPlugins } from "./highlight/codeHighlighterPlugin";
import type { ReasoningMessagePartProps } from "./types";

export function Reasoning({
Expand All @@ -11,6 +12,7 @@ export function Reasoning({
<Streamdown
className="reasoning-markdown"
mode={status.type === "running" ? "streaming" : "static"}
plugins={codeHighlighterPlugins}
>
{text}
</Streamdown>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Streamdown's `plugins.code` seam, filled with our highlighter.
//
// Streamdown ships no highlighter of its own — `CodeHighlighterPlugin` is a
// hole the host fills, and until this module existed nobody filled it, which is
// why every code block in the app rendered as flat text while `shiki` sat
// resolved in the lockfile the whole time.
//
// THE STALENESS TRAP, which is why `highlight` never returns `null`.
// `HighlightedCodeBlockBody` holds its result in `useState(raw)` and its effect
// only writes that state when `highlight` returns a value or calls back:
//
// let r = plugin.highlight({...}, c => setResult(c));
// r && setResult(r);
//
// There is no `else`. Return `null` from a cold grammar and the body keeps
// rendering the result from a PREVIOUS render — during streaming that is the
// first delta, frozen, while the model keeps writing. So a cold or unsupported
// language returns uncoloured tokens for the CURRENT code instead: same text
// streamdown would have shown unplugged, upgraded in place once the grammar
// lands.

import type {
BundledLanguage,
CodeHighlighterPlugin,
PluginConfig,
ThemeInput,
} from "streamdown";

import { getSyntaxHighlighter } from "./syntaxHighlighting";
import {
SYNTAX_LANGUAGES,
resolveSyntaxLanguage,
type SyntaxLine,
} from "./syntaxTokens";

// Streamdown declares these two but does not export them, so they are derived
// from the one member it does export. Re-declaring them by hand would give this
// file its own private copy of a contract it does not own — the shape would
// drift on the next streamdown bump with nothing failing until a code block
// rendered wrong.
type HighlightResult = NonNullable<
ReturnType<CodeHighlighterPlugin["highlight"]>
>;
type HighlightToken = HighlightResult["tokens"][number][number];

/**
* Streamdown asks the plugin which theme pair to hand back to `highlight`, and
* we ignore the answer: our token colours are CSS variables resolved by the
* app's own theme (see `markdown.css`), so there is no light/dark shiki pair to
* choose between. A frozen constant keeps streamdown's context memo stable.
*/
const THEMES: [ThemeInput, ThemeInput] = [
{ name: "copilot-syntax", settings: [] },
{ name: "copilot-syntax", settings: [] },
];

const LANGUAGES = SYNTAX_LANGUAGES as readonly string[] as BundledLanguage[];

const shikiCodePlugin: CodeHighlighterPlugin = {
name: "shiki",
type: "code-highlighter",

getSupportedLanguages: () => LANGUAGES,
getThemes: () => THEMES,
supportsLanguage: (language) => resolveSyntaxLanguage(language) !== null,

highlight({ code, language }, callback) {
const highlighter = getSyntaxHighlighter();
const resolved = resolveSyntaxLanguage(language);
if (!highlighter || !resolved) return plainResult(code);

const ready = highlighter.tokenize(code, resolved);
if (ready) return toHighlightResult(ready);

if (callback) {
// Ordering is what makes a late callback safe. Every call for the same
// language awaits the SAME in-flight load promise, so its continuations
// run in registration order — i.e. in the order the deltas arrived — and
// the newest code is the last to write. No generation counter needed.
void highlighter.load(resolved).then(() => {
const late = highlighter.tokenize(code, resolved);
if (late) callback(toHighlightResult(late));
});
}
return plainResult(code);
},
};

/** The `plugins` prop itself — one frozen object, so streamdown's memo holds. */
export const codeHighlighterPlugins: PluginConfig = Object.freeze({
code: shikiCodePlugin,
});

export { shikiCodePlugin };

function toHighlightResult(lines: readonly SyntaxLine[]): HighlightResult {
return {
tokens: lines.map((line) =>
line.map((token): HighlightToken => {
const style: Record<string, string> = {};
if (token.italic) style["font-style"] = "italic";
if (token.bold) style["font-weight"] = "bold";
if (token.underline) style["text-decoration"] = "underline";
return {
content: token.content,
...(token.color ? { color: token.color } : {}),
...(Object.keys(style).length > 0 ? { htmlStyle: style } : {}),
};
}),
),
};
}

/** One colourless token per line — streamdown's own `raw` shape, rebuilt. */
function plainResult(code: string): HighlightResult {
return { tokens: code.split("\n").map((line) => [{ content: line }]) };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";

import { createShikiHighlighter } from "./shikiHighlighter";
import { MAX_HIGHLIGHTED_CHARS } from "./syntaxTokens";

// These drive the REAL shiki, not a fake. A fake would prove the seam and
// nothing about whether the grammars we named actually load, which is the half
// that can silently fail: `@shikijs/langs/<id>` is a subpath export, and a
// wrong id resolves to a module-not-found at runtime, in a lazy import, inside
// a `catch` that degrades to plain text. Every test here would still pass
// against a stub.
describe("shiki highlighter", () => {
it("answers null before a grammar is resident, and never throws doing it", () => {
const highlighter = createShikiHighlighter();
expect(highlighter.isReady("tsx")).toBe(false);
expect(highlighter.tokenize("const x = 1;", "tsx")).toBeNull();
});

it("colours tokens with CSS variables, never a theme's hex", async () => {
const highlighter = createShikiHighlighter();
await highlighter.load("tsx");

const lines = highlighter.tokenize("// note\nconst answer = 42;", "tsx");
expect(lines).not.toBeNull();
expect(lines).toHaveLength(2);

const colours = lines!.flatMap((line) =>
line
.map((token) => token.color)
.filter((color): color is string => !!color),
);
expect(colours.length).toBeGreaterThan(0);
// The whole reason one tokenizer pass serves dark, light and slate.
for (const colour of colours) {
expect(colour).toMatch(/^var\(--syntax-/);
}

const keyword = lines![1]!.find((token) => token.content === "const");
expect(keyword?.color).toBe("var(--syntax-token-keyword)");
const comment = lines![0]!.map((token) => token.color);
expect(comment).toContain("var(--syntax-token-comment)");
});

it("loads a second grammar without disturbing the first", async () => {
const highlighter = createShikiHighlighter();
await highlighter.load("python");
await highlighter.load("yaml");
expect(highlighter.isReady("python")).toBe(true);
expect(highlighter.isReady("yaml")).toBe(true);
expect(highlighter.tokenize("def f():\n pass", "python")).not.toBeNull();
expect(highlighter.tokenize("a: 1", "yaml")).not.toBeNull();
});

it("is idempotent under concurrent loads of the same grammar", async () => {
const highlighter = createShikiHighlighter();
await Promise.all([
highlighter.load("json"),
highlighter.load("json"),
highlighter.load("json"),
]);
expect(highlighter.tokenize('{"a":1}', "json")).not.toBeNull();
});

it("declines a language it carries no grammar for, without loading anything", async () => {
const highlighter = createShikiHighlighter();
expect(highlighter.supports("cobol")).toBe(false);
await highlighter.load("cobol");
expect(highlighter.isReady("cobol")).toBe(false);
expect(
highlighter.tokenize("IDENTIFICATION DIVISION.", "cobol"),
).toBeNull();
});

it("refuses a block past the size budget rather than tokenizing it per frame", async () => {
const highlighter = createShikiHighlighter();
await highlighter.load("tsx");
const huge = "const x = 1;\n".repeat(
Math.ceil(MAX_HIGHLIGHTED_CHARS / 12) + 1,
);
expect(huge.length).toBeGreaterThan(MAX_HIGHLIGHTED_CHARS);
expect(highlighter.tokenize(huge, "tsx")).toBeNull();
// …and still colours a block just under it.
expect(highlighter.tokenize("const x = 1;", "tsx")).not.toBeNull();
});
});
Loading