-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add Answer agent for automatic post answer generation #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
julianbenegas
commented
Jan 13, 2026
- Add answer jsonb column to posts table with migration
- Create answer-agent.ts with SetAnswer/SetNoAnswer tools using Haiku 4.5
- Trigger answer agent on post creation and new comments
- Skip re-running if post marked as "not-a-question"
- Add PostAnswerBox UI component shown after root comment
- Pass answer through CommentThreadClient to CommentThread
- Add answer jsonb column to posts table with migration - Create answer-agent.ts with SetAnswer/SetNoAnswer tools using Haiku 4.5 - Trigger answer agent on post creation and new comments - Skip re-running if post marked as "not-a-question" - Add PostAnswerBox UI component shown after root comment - Pass answer through CommentThreadClient to CommentThread
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| Answer | ||
| </span> | ||
| </div> | ||
| <div className="prose prose-sm prose-invert max-w-none text-foreground"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The answer.text is rendered as plain text without markdown parsing, causing markdown formatting like bold, lists, and links to appear as literal text in the UI
View Details
📝 Patch Details
diff --git a/app/[owner]/[repo]/[postNumber]/post-answer.tsx b/app/[owner]/[repo]/[postNumber]/post-answer.tsx
index 94d8374..5ca106b 100644
--- a/app/[owner]/[repo]/[postNumber]/post-answer.tsx
+++ b/app/[owner]/[repo]/[postNumber]/post-answer.tsx
@@ -1,6 +1,55 @@
+"use client"
+
+import type { ComponentProps } from "react"
+import { Streamdown } from "streamdown"
import type { PostAnswer } from "@/agent/types"
import { cn } from "@/lib/utils"
+const streamdownComponents: ComponentProps<typeof Streamdown>["components"] = {
+ h1: (props) => <h1 className="mt-6 mb-2 font-semibold text-lg first:mt-0" {...props} />,
+ h2: (props) => <h2 className="mt-6 mb-2 font-semibold text-base first:mt-0" {...props} />,
+ h3: (props) => <h3 className="mt-4 mb-2 font-semibold first:mt-0" {...props} />,
+ h4: (props) => <h4 className="mt-4 mb-2 font-semibold first:mt-0" {...props} />,
+ h5: (props) => <h5 className="mt-4 mb-2 font-semibold first:mt-0" {...props} />,
+ h6: (props) => <h6 className="mt-4 mb-2 font-semibold first:mt-0" {...props} />,
+ p: (props) => <p className="my-2 leading-relaxed first:mt-0 last:mb-0" {...props} />,
+ a: (props) => (
+ <a
+ className="text-blue-400 underline-offset-2 hover:underline"
+ rel="noopener noreferrer"
+ target="_blank"
+ {...props}
+ />
+ ),
+ strong: (props) => <strong className="font-semibold" {...props} />,
+ em: (props) => <em className="italic" {...props} />,
+ ul: (props) => <ul className="my-2 list-disc space-y-1 pl-4" {...props} />,
+ ol: (props) => <ol className="my-2 list-decimal space-y-1 pl-6" {...props} />,
+ li: (props) => <li {...props} />,
+ blockquote: (props) => (
+ <blockquote className="my-2 border-l-2 border-green-400/30 pl-3 italic opacity-75" {...props} />
+ ),
+ hr: () => <hr className="my-2 border-green-400/20" />,
+ code: (props) => (
+ <code
+ className="break-all bg-green-950/40 px-1 py-0.5 font-mono text-sm"
+ {...props}
+ />
+ ),
+ pre: (props) => {
+ // biome-ignore lint/suspicious/noExplicitAny: .
+ const childProps = (props.children as any).props as {
+ className: string
+ children: string
+ }
+ return (
+ <pre className="my-2 overflow-x-auto bg-green-950/40 p-2 text-sm" {...props}>
+ <code>{childProps.children}</code>
+ </pre>
+ )
+ },
+}
+
export function PostAnswerBox({ answer }: { answer: PostAnswer }) {
if (answer.type !== "answer") {
return null
@@ -18,8 +67,14 @@ export function PostAnswerBox({ answer }: { answer: PostAnswer }) {
Answer
</span>
</div>
- <div className="prose prose-sm prose-invert max-w-none text-foreground">
- {answer.text}
+ <div className="text-foreground">
+ <Streamdown
+ components={streamdownComponents}
+ mode="static"
+ shikiTheme={["github-light", "github-dark"]}
+ >
+ {answer.text}
+ </Streamdown>
</div>
</div>
)
Analysis
Why This Bug Exists
The PostAnswerBox component in post-answer.tsx was rendering answer.text directly into a plain div without any markdown parsing:
<div className="prose prose-sm prose-invert max-w-none text-foreground">
{answer.text}
</div>The answer agent (answer-agent.ts) is designed to generate markdown-formatted text - the system prompt instructs it to provide "concise summary" and "summarizing the key points". Agents typically generate markdown naturally with formatting like bold, lists, links, and code blocks.
However, when markdown text is rendered as plain JSX children in a div, it appears as literal text. For example, **bold text** will display as **bold text** instead of rendered bold text.
Meanwhile, elsewhere in the codebase (comment-content.tsx), markdown content is properly parsed using the Streamdown component with custom styled components, demonstrating this is the established pattern in the application.
The Fix
The fix implements markdown parsing for the answer box by:
- Added "use client" directive - Required because Streamdown is a client-side component
- Imported Streamdown and created custom components - Following the same pattern as
comment-content.tsx, created astreamdownComponentsobject that styles markdown elements (headings, lists, code blocks, etc.) to match the green-themed answer box aesthetic - Replaced plain text rendering with Streamdown - Updated the render to:
<Streamdown components={streamdownComponents} mode="static" shikiTheme={["github-light", "github-dark"]} > {answer.text} </Streamdown>
The styling is tailored for the answer box with green accents for consistency with the visual design, but ensures all markdown formatting (bold, lists, links, code blocks, blockquotes, etc.) is properly parsed and displayed.
| </CommentItem> | ||
| {showAnswer && ( | ||
| <div className="mt-8"> | ||
| <PostAnswerBox answer={answer} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PostAnswerBox component type signature requires non-null PostAnswer but receives potentially null answer value
View Details
📝 Patch Details
diff --git a/app/[owner]/[repo]/[postNumber]/post-answer.tsx b/app/[owner]/[repo]/[postNumber]/post-answer.tsx
index 94d8374..05eb933 100644
--- a/app/[owner]/[repo]/[postNumber]/post-answer.tsx
+++ b/app/[owner]/[repo]/[postNumber]/post-answer.tsx
@@ -1,8 +1,8 @@
import type { PostAnswer } from "@/agent/types"
import { cn } from "@/lib/utils"
-export function PostAnswerBox({ answer }: { answer: PostAnswer }) {
- if (answer.type !== "answer") {
+export function PostAnswerBox({ answer }: { answer: PostAnswer | null }) {
+ if (!answer || answer.type !== "answer") {
return null
}
Analysis
Bug Explanation
At line 337 in comment-thread.tsx, PostAnswerBox is called with {answer} where answer is typed as PostAnswer | null | undefined (from line 236: answer?: PostAnswer | null). However, the component's type signature originally declared { answer: PostAnswer }, which does not accept null values.
The guard condition at line 287 is const showAnswer = isRootComment && answer?.type === "answer". While this logically ensures that when showAnswer is true, answer must be valid with type "answer", TypeScript's type narrowing cannot infer this because:
- The condition result is stored in a boolean variable (
showAnswer) - When checking
if (showAnswer)at line 334, TypeScript doesn't understand the original condition - Therefore,
answerremains typed asPostAnswer | null | undefinedrather than being narrowed toPostAnswer
This is a type safety violation where the caller passes a potentially-null value to a component that claims to require a non-null value.
Fix Explanation
I updated the PostAnswerBox component signature from:
export function PostAnswerBox({ answer }: { answer: PostAnswer }) {
if (answer.type !== "answer") {
return null
}To:
export function PostAnswerBox({ answer }: { answer: PostAnswer | null }) {
if (!answer || answer.type !== "answer") {
return null
}This fix:
- Accepts the reality: The component is called with potentially null values due to TypeScript's type narrowing limitations
- Maintains existing logic: The component already had defensive checks; now the type signature reflects this
- Preserves correctness: The guard at line 287 ensures we only render when answer is valid, and the component handles unexpected cases gracefully
- Resolves type mismatch: No type error when calling
PostAnswerBox answer={answer}