mirror of
https://github.com/cline/cline.git
synced 2026-09-13 01:39:57 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c83b4281 |
@@ -14,7 +14,6 @@
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.2",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
|
||||
@@ -10,13 +10,9 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import type {
|
||||
BundledLanguage,
|
||||
BundledTheme,
|
||||
HighlighterGeneric,
|
||||
ThemedToken,
|
||||
} from "shiki";
|
||||
import { createHighlighter } from "shiki";
|
||||
import type { HighlighterCore, ThemedToken } from "shiki/core";
|
||||
import { createHighlighterCore } from "shiki/core";
|
||||
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
@@ -108,10 +104,12 @@ const LineSpan = ({
|
||||
// Types
|
||||
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
||||
code: string;
|
||||
language: BundledLanguage;
|
||||
language: SupportedCodeLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
};
|
||||
|
||||
type SupportedCodeLanguage = "json" | "typescript";
|
||||
|
||||
interface TokenizedCode {
|
||||
tokens: ThemedToken[][];
|
||||
fg: string;
|
||||
@@ -128,10 +126,7 @@ const CodeBlockContext = createContext<CodeBlockContextType>({
|
||||
});
|
||||
|
||||
// Highlighter cache (singleton per language)
|
||||
const highlighterCache = new Map<
|
||||
string,
|
||||
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
|
||||
>();
|
||||
const highlighterCache = new Map<string, Promise<HighlighterCore>>();
|
||||
|
||||
// Token cache
|
||||
const tokensCache = new Map<string, TokenizedCode>();
|
||||
@@ -139,23 +134,31 @@ const tokensCache = new Map<string, TokenizedCode>();
|
||||
// Subscribers for async token updates
|
||||
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
|
||||
|
||||
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
|
||||
const getTokensCacheKey = (code: string, language: SupportedCodeLanguage) => {
|
||||
const start = code.slice(0, 100);
|
||||
const end = code.length > 100 ? code.slice(-100) : "";
|
||||
return `${language}:${code.length}:${start}:${end}`;
|
||||
};
|
||||
|
||||
const getHighlighter = (
|
||||
language: BundledLanguage,
|
||||
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
|
||||
language: SupportedCodeLanguage,
|
||||
): Promise<HighlighterCore> => {
|
||||
const cached = highlighterCache.get(language);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighterPromise = createHighlighter({
|
||||
langs: [language],
|
||||
themes: ["github-light", "github-dark"],
|
||||
const highlighterPromise = createHighlighterCore({
|
||||
engine: createJavaScriptRegexEngine(),
|
||||
langs: [
|
||||
language === "typescript"
|
||||
? () => import("shiki/dist/langs/typescript.mjs")
|
||||
: () => import("shiki/dist/langs/json.mjs"),
|
||||
],
|
||||
themes: [
|
||||
() => import("shiki/dist/themes/github-light.mjs"),
|
||||
() => import("shiki/dist/themes/github-dark.mjs"),
|
||||
],
|
||||
});
|
||||
|
||||
highlighterCache.set(language, highlighterPromise);
|
||||
@@ -181,7 +184,7 @@ const createRawTokens = (code: string): TokenizedCode => ({
|
||||
// Synchronous highlight with callback for async results
|
||||
export const highlightCode = (
|
||||
code: string,
|
||||
language: BundledLanguage,
|
||||
language: SupportedCodeLanguage,
|
||||
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
|
||||
callback?: (result: TokenizedCode) => void,
|
||||
): TokenizedCode | null => {
|
||||
@@ -376,7 +379,7 @@ export const CodeBlockContent = ({
|
||||
showLineNumbers = false,
|
||||
}: {
|
||||
code: string;
|
||||
language: BundledLanguage;
|
||||
language: SupportedCodeLanguage;
|
||||
showLineNumbers?: boolean;
|
||||
}) => {
|
||||
// Memoized raw tokens for immediate display
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { code } from "@streamdown/code";
|
||||
import { math } from "@streamdown/math";
|
||||
import { mermaid } from "@streamdown/mermaid";
|
||||
import type { UIMessage } from "ai";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
@@ -25,6 +21,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { streamdownPlugins } from "@/lib/streamdown-plugins";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
@@ -318,8 +315,6 @@ export const MessageBranchPage = ({
|
||||
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<Streamdown
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export interface ReasoningContextValue {
|
||||
isStreaming: boolean;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
duration: number | undefined;
|
||||
}
|
||||
|
||||
export const ReasoningContext = createContext<ReasoningContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export const useReasoning = () => {
|
||||
const context = useContext(ReasoningContext);
|
||||
if (!context) {
|
||||
throw new Error("Reasoning components must be used within Reasoning");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -1,49 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { code } from "@streamdown/code";
|
||||
import { math } from "@streamdown/math";
|
||||
import { mermaid } from "@streamdown/mermaid";
|
||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { streamdownPlugins } from "@/lib/streamdown-plugins";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { ReasoningContext, useReasoning } from "./reasoning-context";
|
||||
import { Shimmer } from "./shimmer";
|
||||
|
||||
interface ReasoningContextValue {
|
||||
isStreaming: boolean;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
duration: number | undefined;
|
||||
}
|
||||
|
||||
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
||||
|
||||
export const useReasoning = () => {
|
||||
const context = useContext(ReasoningContext);
|
||||
if (!context) {
|
||||
throw new Error("Reasoning components must be used within Reasoning");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
||||
isStreaming?: boolean;
|
||||
open?: boolean;
|
||||
@@ -204,8 +176,6 @@ export type ReasoningContentProps = ComponentProps<
|
||||
children: string;
|
||||
};
|
||||
|
||||
const streamdownPlugins = { cjk, code, math, mermaid };
|
||||
|
||||
export const ReasoningContent = memo(
|
||||
({ className, children, ...props }: ReasoningContentProps) => (
|
||||
<CollapsibleContent
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { cjk } from "@streamdown/cjk";
|
||||
import { math } from "@streamdown/math";
|
||||
import { createHighlighterCore, type HighlighterCore } from "shiki/core";
|
||||
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
|
||||
import {
|
||||
type BundledLanguage,
|
||||
bundledLanguages,
|
||||
bundledLanguagesInfo,
|
||||
} from "shiki/langs";
|
||||
import type {
|
||||
CodeHighlighterPlugin,
|
||||
DiagramPlugin,
|
||||
PluginConfig,
|
||||
ThemeInput,
|
||||
} from "streamdown";
|
||||
|
||||
interface HighlightResult {
|
||||
bg?: string;
|
||||
fg?: string;
|
||||
rootStyle?: string | false;
|
||||
tokens: {
|
||||
bgColor?: string;
|
||||
color?: string;
|
||||
content: string;
|
||||
htmlAttrs?: Record<string, string>;
|
||||
htmlStyle?: Record<string, string>;
|
||||
offset?: number;
|
||||
}[][];
|
||||
}
|
||||
|
||||
const supportedLanguages = Object.keys(bundledLanguages) as BundledLanguage[];
|
||||
const languageAliases = new Map<string, BundledLanguage>(
|
||||
bundledLanguagesInfo.flatMap((language) =>
|
||||
(language.aliases ?? []).map((alias) => [
|
||||
alias,
|
||||
language.id as BundledLanguage,
|
||||
]),
|
||||
),
|
||||
);
|
||||
const supportedLanguageSet = new Set<string>(supportedLanguages);
|
||||
const defaultThemes = ["github-light", "github-dark"] as [
|
||||
ThemeInput,
|
||||
ThemeInput,
|
||||
];
|
||||
|
||||
const highlighterCache = new Map<string, Promise<HighlighterCore>>();
|
||||
const tokenCache = new Map<string, HighlightResult>();
|
||||
const subscribers = new Map<string, Set<(result: HighlightResult) => void>>();
|
||||
|
||||
const normalizeLanguage = (language: string): BundledLanguage | null => {
|
||||
const normalized = language.trim().toLowerCase();
|
||||
if (supportedLanguageSet.has(normalized)) {
|
||||
return normalized as BundledLanguage;
|
||||
}
|
||||
|
||||
return languageAliases.get(normalized) ?? null;
|
||||
};
|
||||
|
||||
const getThemeName = (theme: ThemeInput) =>
|
||||
typeof theme === "string" ? theme : theme.name;
|
||||
|
||||
const getCacheKey = (
|
||||
code: string,
|
||||
language: BundledLanguage,
|
||||
themes: [ThemeInput, ThemeInput],
|
||||
) => {
|
||||
const start = code.slice(0, 100);
|
||||
const end = code.length > 100 ? code.slice(-100) : "";
|
||||
return `${language}:${getThemeName(themes[0])}:${getThemeName(themes[1])}:${code.length}:${start}:${end}`;
|
||||
};
|
||||
|
||||
const getHighlighter = (language: BundledLanguage) => {
|
||||
const cached = highlighterCache.get(language);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const highlighter = createHighlighterCore({
|
||||
engine: createJavaScriptRegexEngine({ forgiving: true }),
|
||||
langs: [bundledLanguages[language]],
|
||||
themes: [
|
||||
() => import("shiki/dist/themes/github-light.mjs"),
|
||||
() => import("shiki/dist/themes/github-dark.mjs"),
|
||||
],
|
||||
});
|
||||
|
||||
highlighterCache.set(language, highlighter);
|
||||
return highlighter;
|
||||
};
|
||||
|
||||
const createPlainResult = (code: string): HighlightResult => ({
|
||||
bg: "transparent",
|
||||
fg: "inherit",
|
||||
tokens: code
|
||||
.split("\n")
|
||||
.map((line) => (line === "" ? [] : [{ color: "inherit", content: line }])),
|
||||
});
|
||||
|
||||
const code: CodeHighlighterPlugin = {
|
||||
getSupportedLanguages: () => [...supportedLanguages],
|
||||
getThemes: () => defaultThemes,
|
||||
highlight: ({ code: source, language, themes }, callback) => {
|
||||
const normalizedLanguage = normalizeLanguage(language);
|
||||
if (!normalizedLanguage) {
|
||||
return createPlainResult(source);
|
||||
}
|
||||
|
||||
const cacheKey = getCacheKey(source, normalizedLanguage, themes);
|
||||
const cached = tokenCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
if (!subscribers.has(cacheKey)) {
|
||||
subscribers.set(cacheKey, new Set());
|
||||
}
|
||||
subscribers.get(cacheKey)?.add(callback);
|
||||
}
|
||||
|
||||
getHighlighter(normalizedLanguage)
|
||||
.then((highlighter) => {
|
||||
const lightTheme = getThemeName(themes[0]);
|
||||
const darkTheme = getThemeName(themes[1]);
|
||||
const result = highlighter.codeToTokens(source, {
|
||||
lang: normalizedLanguage,
|
||||
themes: {
|
||||
dark: darkTheme,
|
||||
light: lightTheme,
|
||||
},
|
||||
});
|
||||
|
||||
tokenCache.set(cacheKey, result);
|
||||
|
||||
const cacheSubscribers = subscribers.get(cacheKey);
|
||||
if (cacheSubscribers) {
|
||||
for (const subscriber of cacheSubscribers) {
|
||||
subscriber(result);
|
||||
}
|
||||
subscribers.delete(cacheKey);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("[Streamdown Code] Failed to highlight code:", error);
|
||||
subscribers.delete(cacheKey);
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
name: "shiki",
|
||||
supportsLanguage: (language) => normalizeLanguage(language) !== null,
|
||||
type: "code-highlighter",
|
||||
};
|
||||
|
||||
type MermaidConfig = NonNullable<Parameters<DiagramPlugin["getMermaid"]>[0]>;
|
||||
type MermaidModule = typeof import("@streamdown/mermaid");
|
||||
|
||||
const defaultMermaidConfig = {
|
||||
fontFamily: "monospace",
|
||||
securityLevel: "strict",
|
||||
startOnLoad: false,
|
||||
suppressErrorRendering: true,
|
||||
theme: "default",
|
||||
} satisfies MermaidConfig;
|
||||
|
||||
let mermaidModulePromise: Promise<MermaidModule> | null = null;
|
||||
let mermaidConfig: MermaidConfig = defaultMermaidConfig;
|
||||
let pendingMermaidInitialize = true;
|
||||
|
||||
const getMermaidModule = () => {
|
||||
if (!mermaidModulePromise) {
|
||||
mermaidModulePromise = import("@streamdown/mermaid");
|
||||
}
|
||||
return mermaidModulePromise;
|
||||
};
|
||||
|
||||
const lazyMermaid: DiagramPlugin = {
|
||||
getMermaid: (config) => {
|
||||
if (config) {
|
||||
mermaidConfig = {
|
||||
...defaultMermaidConfig,
|
||||
...mermaidConfig,
|
||||
...config,
|
||||
};
|
||||
pendingMermaidInitialize = true;
|
||||
}
|
||||
|
||||
return {
|
||||
initialize: (nextConfig) => {
|
||||
mermaidConfig = {
|
||||
...defaultMermaidConfig,
|
||||
...nextConfig,
|
||||
};
|
||||
pendingMermaidInitialize = true;
|
||||
void getMermaidModule().then(({ mermaid }) => {
|
||||
mermaid.getMermaid().initialize(mermaidConfig);
|
||||
pendingMermaidInitialize = false;
|
||||
});
|
||||
},
|
||||
render: async (id, source) => {
|
||||
const { mermaid } = await getMermaidModule();
|
||||
const mermaidInstance = mermaid.getMermaid();
|
||||
if (pendingMermaidInitialize) {
|
||||
mermaidInstance.initialize(mermaidConfig);
|
||||
pendingMermaidInitialize = false;
|
||||
}
|
||||
return mermaidInstance.render(id, source);
|
||||
},
|
||||
};
|
||||
},
|
||||
language: "mermaid",
|
||||
name: "mermaid",
|
||||
type: "diagram",
|
||||
};
|
||||
|
||||
export const streamdownPlugins = {
|
||||
cjk,
|
||||
code,
|
||||
math,
|
||||
mermaid: lazyMermaid,
|
||||
} satisfies PluginConfig;
|
||||
@@ -3,6 +3,39 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const nodeModuleChunk = (moduleId: string) => {
|
||||
if (!moduleId.includes("node_modules")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/[\\/]node_modules[\\/](shiki|@shikijs|mermaid)[\\/]/.test(moduleId)) {
|
||||
return null;
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/.test(moduleId)) {
|
||||
return "vendor-react";
|
||||
}
|
||||
if (
|
||||
/[\\/]node_modules[\\/](@base-ui|@radix-ui|cmdk|lucide-react|sonner)[\\/]/.test(
|
||||
moduleId,
|
||||
)
|
||||
) {
|
||||
return "vendor-ui";
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](@xyflow|recharts|d3-)[\\/]/.test(moduleId)) {
|
||||
return "vendor-visualization";
|
||||
}
|
||||
if (/[\\/]node_modules[\\/](ai|tokenlens)[\\/]/.test(moduleId)) {
|
||||
return "vendor-ai";
|
||||
}
|
||||
|
||||
const packageMatch = moduleId
|
||||
.split(/[\\/]node_modules[\\/]/)
|
||||
.pop()
|
||||
?.match(/^(@[^\\/]+[\\/][^\\/]+|[^\\/]+)/);
|
||||
const packageName = packageMatch?.[1]?.replace(/[\\/@]/g, "-");
|
||||
return packageName ? `vendor-${packageName}` : "vendor";
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
@@ -25,5 +58,18 @@ export default defineConfig({
|
||||
outDir: "../../dist/webview",
|
||||
emptyOutDir: true,
|
||||
cssMinify: "esbuild",
|
||||
chunkSizeWarningLimit: 800,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
{
|
||||
name: nodeModuleChunk,
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"apps/cli": {
|
||||
"name": "@cline/cli",
|
||||
"version": "3.0.17",
|
||||
"version": "3.0.20",
|
||||
"bin": {
|
||||
"cline": "src/index.ts",
|
||||
},
|
||||
@@ -78,7 +78,6 @@
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@rive-app/react-webgl2": "^4.27.2",
|
||||
"@streamdown/cjk": "^1.0.3",
|
||||
"@streamdown/code": "^1.1.1",
|
||||
"@streamdown/math": "^1.0.2",
|
||||
"@streamdown/mermaid": "^1.0.2",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
|
||||
Reference in New Issue
Block a user