mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 00:48:10 +00:00
fix markdown display (#1199)
This commit is contained in:
+2
-2
@@ -103,7 +103,7 @@ ClawXは公式の**OpenClaw**コアを直接ベースに構築されています
|
||||
インストールから最初のAIインタラクションまで、すべてのセットアップを直感的なグラフィカルインターフェースで完了できます。ターミナルコマンド不要、YAMLファイル不要、環境変数の探索も不要です。
|
||||
|
||||
### 💬 インテリジェントチャットインターフェース
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、Markdownによるリッチコンテンツレンダリング(GitHub 風テーブルや KaTeX による LaTeX 数式 `$インライン$`、`$$ブロック$$`、`\(インライン\)`、`\[ブロック\]` を含む)に加え、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。
|
||||
モダンなチャット体験を通じてAIエージェントとコミュニケーションできます。複数の会話コンテキスト、メッセージ履歴、エージェント応答の Markdown レンダリング(GitHub 風テーブルや KaTeX による LaTeX 数式 `$インライン$`、`$$ブロック$$`、`\(インライン\)`、`\[ブロック\]` を含む)をサポートし、ユーザー入力は常にプレーンテキストとして表示します。さらに、マルチエージェント構成ではメイン入力欄の `@agent` から対象エージェントへ直接ルーティングできます。
|
||||
コンポーザーから挿入した Skill は `/skill-name` 形式のチップとして表示され、チップをクリックすると右側のプレビュー側欄でその Skill の `SKILL.md` を開けます。
|
||||
`@agent` で別のエージェントを選ぶと、ClawX はデフォルトエージェントを経由せず、そのエージェント自身の会話コンテキストへ直接切り替えます。各エージェントのワークスペースは既定で分離されていますが、より強い実行時分離は OpenClaw の sandbox 設定に依存します。
|
||||
セッション側欄はワークスペース優先で整理され、既定ワークスペースを先頭に固定し、その他のワークスペースは自然順に並べます。各ワークスペースは折りたたみや追加読み込みができます。AI の返信中は行にスピナーが表示され、未確認の返信が完了すると青い点に変わり、会話を開くと相対アクティビティ時刻に戻ります。ホバーすると引き続き操作ボタンが表示されます。インポートしたワークスペースは側欄の見出しから名前を変更でき、新しい名前はチャット入力欄の下にも反映されます。見出しにホバーすると引き続きファイルシステムのパスを確認できます。選択中の会話に有効なワークスペースがある場合、新しいチャットはそれを引き継ぎ、最初の送信までは変更できます。編集可能な新規または未バインドのチャットでは、コンポーザーのワークスペースチップから最近使用したワークスペースと既存セッションのワークスペースの一覧を開き、既定ワークスペースへ戻すか別フォルダーを選べます。保存済みのワークスペースフォルダーが移動または削除されている場合、Chat はセッション作成を一時停止し、無効なパスを繰り返し再試行せずに既存のフォルダーを選ぶよう案内します。利用できない既定以外のグループには側欄で印が付き、確認後に削除できます。この操作ではグループ内の全セッションが完全に削除されます。OpenClaw が生成する UUID と日付のフォールバックタイトルは、そのセッション ID と一致する場合に限って欠落タイトルとして扱い、セッション名として保存せず、会話の最初のユーザーメッセージに置き換えて表示します。
|
||||
@@ -268,7 +268,7 @@ ACP Chat は、runtime が画像生成メディアを信頼できる構造化メ
|
||||
│ │ • モダンなコンポーネントベースUI(React 19) │ │
|
||||
│ │ • Zustandによるステート管理 │ │
|
||||
│ │ • 統一 host-api/api-client 呼び出し │ │
|
||||
│ │ • リッチなMarkdownレンダリング │ │
|
||||
│ │ • 応答はMarkdown、ユーザー入力はプレーンテキスト │ │
|
||||
│ └──────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
|
||||
@@ -103,7 +103,7 @@ When Developer Mode is enabled, the sidebar also provides a native Dreams page f
|
||||
Complete the entire setup—from installation to your first AI interaction—through an intuitive graphical interface. No terminal commands, no YAML files, no environment variable hunting.
|
||||
|
||||
### 💬 Intelligent Chat Interface
|
||||
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, rich content rendering with Markdown (including GitHub-flavored tables and KaTeX-powered LaTeX math: `$inline$`, `$$block$$`, `\(inline\)`, and `\[block\]`), and direct `@agent` routing in the main composer for multi-agent setups.
|
||||
Communicate with AI agents through a modern chat experience. Support for multiple conversation contexts, message history, assistant replies rendered with Markdown (including GitHub-flavored tables and KaTeX-powered LaTeX math: `$inline$`, `$$block$$`, `\(inline\)`, and `\[block\]`) while user input remains literal text, and direct `@agent` routing in the main composer for multi-agent setups.
|
||||
Skills you insert from the composer appear as `/skill-name` chips; click a chip to open the preview sidebar and read that skill's `SKILL.md`.
|
||||
When you target another agent with `@agent`, ClawX switches into that agent's own conversation context directly instead of relaying through the default agent. Agent workspaces stay separate by default, and stronger isolation depends on OpenClaw sandbox settings.
|
||||
The session sidebar is workspace-first: the default workspace stays at the top, other workspaces sort naturally, and each workspace can collapse or load more sessions. A row shows a spinner while the AI is replying, a blue dot when an unseen reply finishes, and its relative activity time after the conversation is opened; hovering still reveals row actions. Imported workspaces can be renamed from their sidebar header; the custom name is reflected in the chat composer while hovering the header still reveals the filesystem path. When available, a new chat inherits the selected conversation's workspace while remaining editable until first send. Editable new or unbound chats expose the composer workspace chip as a small menu that lists recent and known-session workspaces, returns to the default workspace, or chooses another folder. If a saved workspace folder was moved or deleted, Chat pauses session creation and prompts you to choose an existing folder instead of repeatedly retrying the missing path. Unavailable non-default groups are marked in the sidebar and can be removed after confirmation; this permanently deletes every session in that group. Synthetic OpenClaw UUID-date fallback titles are treated as missing only when they match the session ID, then replaced with the conversation's first user prompt instead of being persisted as the session name.
|
||||
@@ -271,7 +271,7 @@ ACP Chat can also display generated image previews when image-generation media i
|
||||
│ │ • Modern component-based UI (React 19) │ │
|
||||
│ │ • State management with Zustand │ │
|
||||
│ │ • Unified host-api/api-client calls │ │
|
||||
│ │ • Rich Markdown rendering │ │
|
||||
│ │ • Markdown assistant replies, literal user input │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
|
||||
+2
-2
@@ -101,7 +101,7 @@ ClawX построен непосредственно на официально
|
||||
Весь процесс — от установки до первого взаимодействия с AI — выполняется через интуитивный графический интерфейс. Без терминальных команд, без YAML-файлов, без поиска переменных окружения.
|
||||
|
||||
### 💬 Интеллектуальный интерфейс чата
|
||||
Общайтесь с AI-агентами через современный чат. Поддержка нескольких контекстов разговора, истории сообщений, рендеринга Markdown (включая таблицы GitHub-flavored и математические формулы LaTeX через KaTeX: `$строчные$`, `$$блочные$$`, `\(строчные\)` и `\[блочные\]`) и прямая маршрутизация через `@agent` в главном поле ввода для мультиагентных конфигураций.
|
||||
Общайтесь с AI-агентами через современный чат. Поддержка нескольких контекстов разговора, истории сообщений и рендеринга ответов агента в Markdown (включая таблицы GitHub-flavored и математические формулы LaTeX через KaTeX: `$строчные$`, `$$блочные$$`, `\(строчные\)` и `\[блочные\]`), при этом пользовательский ввод всегда отображается как обычный текст. Для мультиагентных конфигураций также доступна прямая маршрутизация через `@agent` в главном поле ввода.
|
||||
Навыки, вставляемые из поля ввода, отображаются как чипы `/skill-name`; нажмите на чип, чтобы открыть боковую панель предпросмотра и прочитать `SKILL.md` соответствующего навыка.
|
||||
При выборе другого агента через `@agent` ClawX переключается непосредственно в контекст этого агента вместо ретрансляции через агента по умолчанию. Рабочие пространства агентов по умолчанию разделены, но более строгая изоляция зависит от настроек песочницы OpenClaw.
|
||||
Каждый агент может переопределить свои настройки `provider/model`; агенты без переопределения продолжают наследовать глобальную модель по умолчанию.
|
||||
@@ -237,7 +237,7 @@ ClawX использует **двухпроцессную архитектуру
|
||||
│ │ • Современный UI на компонентах (React 19) │ │
|
||||
│ │ • Управление состоянием с Zustand │ │
|
||||
│ │ • Унифицированные вызовы host-api/api-client │ │
|
||||
│ │ • Рендеринг Markdown │ │
|
||||
│ │ • Ответы в Markdown, ввод как обычный текст │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
|
||||
+2
-2
@@ -104,7 +104,7 @@ ClawX 直接基于官方 **OpenClaw** 核心构建。无需单独安装,我们
|
||||
从安装到第一次 AI 对话,全程通过直观的图形界面完成。无需终端命令,无需 YAML 文件,无需到处寻找环境变量。
|
||||
|
||||
### 💬 智能聊天界面
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、Markdown 富文本渲染(包括 GitHub 风格表格以及由 KaTeX 渲染的 LaTeX 数学公式:`$行内$`、`$$块级$$`、`\(行内\)` 和 `\[块级\]`),以及在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。
|
||||
通过现代化的聊天体验与 AI 智能体交互。支持多会话上下文、消息历史记录、以 Markdown 渲染智能体回复(包括 GitHub 风格表格以及由 KaTeX 渲染的 LaTeX 数学公式:`$行内$`、`$$块级$$`、`\(行内\)` 和 `\[块级\]`),用户输入则始终按原始文本显示;同时支持在多 Agent 场景下通过主输入框中的 `@agent` 直接路由到目标智能体。
|
||||
从输入框插入的技能会以 `/技能名` 卡片形式显示;点击卡片可在右侧预览栏打开并阅读该技能的 `SKILL.md`。
|
||||
当你使用 `@agent` 选择其他智能体时,ClawX 会直接切换到该智能体自己的对话上下文,而不是经过默认智能体转发。各 Agent 工作区默认彼此分离,但更强的运行时隔离仍取决于 OpenClaw 的 sandbox 配置。
|
||||
会话侧边栏现在以工作空间优先组织:默认工作空间固定在最上方,其它工作空间按自然顺序排列,每个工作空间都可折叠或继续加载更多会话。AI 回复期间,会话行显示加载指示器;未查看的回复完成后显示蓝点;打开会话后恢复显示相对活跃时间,悬停时仍会露出操作按钮。导入的工作空间可从侧边栏标题处重命名,新名称会同步显示在对话输入框下方,同时悬浮标题仍可查看文件系统路径。如果当前所选会话存在有效工作空间,新对话会继承该工作空间,并在首次发送前保持可编辑。对于可编辑的新对话或未绑定对话,输入框的工作空间卡片会打开一个小菜单,列出最近使用及现有会话中的工作空间,并可切回默认工作空间或选择其它目录。如果保存的工作空间文件夹已被移动或删除,Chat 会暂停创建会话并提示选择现有文件夹,而不会持续重试失效路径。不可用的非默认工作空间会在侧边栏显示标记,并可在确认后删除;该操作会永久删除分组中的全部会话。OpenClaw 生成的 UUID 加日期兜底标题只有在与该会话 ID 匹配时才会被视为缺失标题,随后改用会话的首条用户消息展示,而不会被持久化为会话名称。
|
||||
@@ -272,7 +272,7 @@ ACP Chat 也可在 runtime 以可信结构化媒体投递图像生成结果时
|
||||
│ │ • 现代组件化 UI(React 19) │ │
|
||||
│ │ • Zustand 状态管理 │ │
|
||||
│ │ • 统一 host-api/api-client 调用 │ │
|
||||
│ │ • Markdown 富文本渲染 │ │
|
||||
│ │ • 回复使用 Markdown,用户输入按原文显示 │ │
|
||||
│ └────────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
|
||||
@@ -26,21 +26,11 @@ function normalizeLatexDelimiters(input: string): string {
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function AcpMarkdownPart({ text, tone }: { text: string; tone: RenderTone }) {
|
||||
function AcpMarkdownPart({ text }: { text: string }) {
|
||||
const { t } = useTranslation('chat');
|
||||
const isUser = tone === 'user';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'prose prose-sm max-w-none break-words',
|
||||
isUser
|
||||
? // User messages skip full markdown rendering, so preserve \n line breaks in paragraphs
|
||||
'prose-invert text-white [&_*]:text-inherit [&_p]:whitespace-pre-wrap'
|
||||
: // AI replies render as full markdown where code blocks/lists handle line breaks natively
|
||||
'dark:prose-invert text-foreground',
|
||||
)}
|
||||
>
|
||||
<div className="prose prose-sm max-w-none break-words text-foreground dark:prose-invert">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[[rehypeKatex, { strict: false, throwOnError: false, output: 'html' }]]}
|
||||
@@ -160,11 +150,11 @@ export function AcpRenderPart({ part, tone = 'assistant' }: { part: RenderPart;
|
||||
if (tone === 'user') {
|
||||
return (
|
||||
<div className="rounded-2xl bg-brand px-4 py-3 text-white shadow-sm">
|
||||
<AcpMarkdownPart text={part.text} tone={tone} />
|
||||
<p className="whitespace-pre-wrap break-words">{part.text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <AcpMarkdownPart text={part.text} tone={tone} />;
|
||||
return <AcpMarkdownPart text={part.text} />;
|
||||
}
|
||||
|
||||
if (part.kind === 'image') return <AcpImagePart part={part} />;
|
||||
|
||||
@@ -24,7 +24,7 @@ const seededUpdates: AcpSessionUpdate[] = [
|
||||
{
|
||||
sessionUpdate: 'user_message',
|
||||
messageId: 'plain-markdown-user',
|
||||
content: [{ type: 'text', text: 'Please render a Markdown reply plainly.' }],
|
||||
content: [{ type: 'text', text: '**Please** render `this input` literally.\n# Not a heading' }],
|
||||
},
|
||||
{
|
||||
sessionUpdate: 'agent_message',
|
||||
@@ -65,8 +65,8 @@ async function emitAcpSessionUpdates(app: ElectronApplication, updates: AcpSessi
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('ClawX assistant reply Markdown styling', () => {
|
||||
test('renders assistant text as plain Markdown while keeping user prompts bubbled', async ({ launchElectronApp }, testInfo) => {
|
||||
test.describe('ClawX chat Markdown styling', () => {
|
||||
test('renders assistant Markdown while keeping user input literal and bubbled', async ({ launchElectronApp }, testInfo) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
@@ -138,8 +138,10 @@ test.describe('ClawX assistant reply Markdown styling', () => {
|
||||
root.classList.add('light');
|
||||
});
|
||||
|
||||
const userBubble = page.getByTestId('acp-user-message').filter({ hasText: 'Please render a Markdown reply plainly.' }).locator('div.rounded-2xl.bg-brand').first();
|
||||
const userBubble = page.getByTestId('acp-user-message').filter({ hasText: 'Please' }).locator('div.rounded-2xl.bg-brand').first();
|
||||
await expect(userBubble).toBeVisible({ timeout: 30_000 });
|
||||
await expect(userBubble.locator('p')).toHaveText('**Please** render `this input` literally.\n# Not a heading');
|
||||
await expect(userBubble.locator('strong, code, h1')).toHaveCount(0);
|
||||
|
||||
const assistantProse = page.getByTestId('acp-assistant-message').filter({ hasText: 'Plain Markdown reply' }).locator('.prose').first();
|
||||
await expect(assistantProse).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
@@ -212,6 +212,34 @@ describe('ACP chat timeline components', () => {
|
||||
expect(inlineCode).not.toHaveClass('bg-black/5');
|
||||
});
|
||||
|
||||
it('renders user input as literal text instead of Markdown', () => {
|
||||
const userText = '**bold** and `inlineCode()`\n# heading\n- list item';
|
||||
const state = snapshot({
|
||||
itemOrder: ['msg-u:0'],
|
||||
itemsById: {
|
||||
'msg-u:0': {
|
||||
kind: 'message-segment',
|
||||
id: 'msg-u:0',
|
||||
role: 'user',
|
||||
messageId: 'msg-u',
|
||||
segmentIndex: 0,
|
||||
parts: [{ kind: 'markdown', text: userText }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<AcpTimeline snapshot={state} />);
|
||||
|
||||
const userMessage = screen.getByTestId('acp-user-message');
|
||||
const paragraph = userMessage.querySelector('p');
|
||||
expect(paragraph?.textContent).toBe(userText);
|
||||
expect(paragraph).toHaveClass('whitespace-pre-wrap');
|
||||
expect(userMessage.querySelector('strong')).not.toBeInTheDocument();
|
||||
expect(userMessage.querySelector('code')).not.toBeInTheDocument();
|
||||
expect(userMessage.querySelector('h1')).not.toBeInTheDocument();
|
||||
expect(userMessage.querySelector('li')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a fixed whole-turn duration on the ACP-replayed assistant turn', () => {
|
||||
const state = snapshot({
|
||||
itemOrder: ['user-a:0', 'assistant-a:0'],
|
||||
|
||||
Reference in New Issue
Block a user