diff --git a/apps/api/src/services/conversationsService.ts b/apps/api/src/services/conversationsService.ts
index 48667d8..987934f 100644
--- a/apps/api/src/services/conversationsService.ts
+++ b/apps/api/src/services/conversationsService.ts
@@ -63,7 +63,9 @@ export class ConversationService {
await this.generateRequirementsDocFromSession(sessionId);
const response =
"これまでの対話を基に、要件定義書を生成しました。\n\n" +
- requirementsDoc;
+ "```md\n" +
+ requirementsDoc +
+ "\n```";
return this.saveMessage(sessionId, "ai", response);
} catch (error) {
console.error("要件定義書生成中にエラー:", error);
@@ -394,8 +396,9 @@ export class ConversationService {
const reqDoc = await this.generateRequirementsDocFromSession(sessionId);
return (
"ここまでの対話を基に、要件定義書を作成しました。\n\n" +
+ "```md\n" +
reqDoc +
- "\n\nこの内容でよろしければ、Issue案を生成します。よろしいですか?"
+ "\n```\n\nこの内容でよろしければ、Issue案を生成します。よろしいですか?"
);
} catch (error) {
console.error("要件定義書生成中にエラーが発生しました:", error);
diff --git a/apps/extension/src/components/MarkdownRenderer.tsx b/apps/extension/src/components/MarkdownRenderer.tsx
new file mode 100644
index 0000000..c5bd541
--- /dev/null
+++ b/apps/extension/src/components/MarkdownRenderer.tsx
@@ -0,0 +1,137 @@
+import { memo, useState } from "react";
+
+interface MarkdownRendererProps {
+ content: string;
+}
+
+// 極力シンプルなMarkdownレンダラー
+// - 見出し: #, ##, ###
+// - 箇条書き: - で始まる行(連続行を
にまとめる)
+// - 空行は段落の区切り
+export const MarkdownRenderer = memo((props: MarkdownRendererProps) => {
+ const { content } = props;
+
+ // ブロック単位で要素を生成
+ const lines = content.split("\n");
+ const elements: React.ReactNode[] = [];
+
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+
+ // コードブロック ```lang\n ... \n```
+ if (line.startsWith("```")) {
+ const _lang = line.slice(3).trim();
+ const codeLines: string[] = [];
+ let j = i + 1;
+ while (j < lines.length && !lines[j].startsWith("```")) {
+ codeLines.push(lines[j]);
+ j += 1;
+ }
+ // j は閉じ```の行位置 or 行末
+ const code = codeLines.join("\n");
+ const Copyable = () => {
+ const [copied, setCopied] = useState(false);
+ const onCopy = async () => {
+ try {
+ await navigator.clipboard.writeText(code);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 1200);
+ } catch {}
+ };
+ return (
+
+
+
+ {code}
+
+
+
+
+ );
+ };
+ elements.push();
+ i = j < lines.length ? j + 1 : j;
+ continue;
+ }
+
+ // 箇条書きブロック
+ if (line.trim().startsWith("- ")) {
+ const items: string[] = [];
+ let j = i;
+ while (j < lines.length && lines[j].trim().startsWith("- ")) {
+ items.push(lines[j].trim().slice(2));
+ j += 1;
+ }
+ elements.push(
+
+ {items.map((item) => {
+ const key = `li-${i}-${item}-${Math.random().toString(36).slice(2, 8)}`;
+ return (
+ -
+ {item}
+
+ );
+ })}
+
,
+ );
+ i = j;
+ continue;
+ }
+
+ // 見出し
+ if (line.startsWith("### ")) {
+ elements.push(
+
+ {line.slice(4)}
+
,
+ );
+ i += 1;
+ continue;
+ }
+ if (line.startsWith("## ")) {
+ elements.push(
+
+ {line.slice(3)}
+
,
+ );
+ i += 1;
+ continue;
+ }
+ if (line.startsWith("# ")) {
+ elements.push(
+
+ {line.slice(2)}
+
,
+ );
+ i += 1;
+ continue;
+ }
+
+ // 空行 → 間隔
+ if (line.trim() === "") {
+ elements.push();
+ i += 1;
+ continue;
+ }
+
+ // 段落
+ elements.push(
+
+ {line}
+
,
+ );
+ i += 1;
+ }
+
+ return {elements}
;
+});
+
+MarkdownRenderer.displayName = "MarkdownRenderer";
diff --git a/apps/extension/src/components/ModelResponse.tsx b/apps/extension/src/components/ModelResponse.tsx
index 3192a98..bf038c7 100644
--- a/apps/extension/src/components/ModelResponse.tsx
+++ b/apps/extension/src/components/ModelResponse.tsx
@@ -1,6 +1,7 @@
import { memo, useState } from "react";
import PrismLogo from "@assets/prism.png";
import { SelectRepository } from "./SelectRepository";
+import { MarkdownRenderer } from "./MarkdownRenderer";
import { useCreateIssues } from "@/hooks/api/createIssues";
import { useGetSessionIssues } from "@/hooks/api/getSessionIssues";
import { useChatStore } from "@/store/chatStore";
@@ -72,7 +73,10 @@ export const ModelResponse = memo((props: ModelResponseProps) => {
- {displayContent}
+ {/* Markdown本文は素のレイアウト。コードブロックのみMarkdownRenderer内で背景化 */}
+
+
+
{shouldShowRepositorySelection && (