Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"use client"

import type { InferSelectModel } from "drizzle-orm"
import { useEffect, useMemo, useState } from "react"
import { authClient } from "@/lib/auth-client"
import { useEffect, useMemo } from "react"
import type {
comments as commentsSchema,
mentions as mentionsSchema,
Expand All @@ -22,13 +21,6 @@ type AuthorInfo = {
isLlm: boolean
}

type AskingOption = {
id: string
name: string
image?: string | null
isDefault?: boolean
}

export function CommentThreadClient({
owner,
repo,
Expand All @@ -38,7 +30,6 @@ export function CommentThreadClient({
reactions,
rootCommentId,
commentNumbers,
askingOptions,
}: {
owner: string
repo: string
Expand All @@ -48,10 +39,7 @@ export function CommentThreadClient({
reactions: Reaction[]
rootCommentId: string | null
commentNumbers: Map<string, string>
askingOptions: AskingOption[]
}) {
const [replyingToId, setReplyingToId] = useState<string | null>(null)
const isSignedIn = !!authClient.useSession().data?.session
const { selectedRef, gitContext } = usePostMetadata()
const currentSha = gitContext?.sha ?? null

Expand Down Expand Up @@ -85,24 +73,12 @@ export function CommentThreadClient({

return (
<CommentThread
askingOptions={askingOptions}
authorsById={authorsById}
commentNumbers={commentNumbers}
comments={filteredComments}
mentions={mentions}
onCancelReply={() => {
if (isSignedIn) {
setReplyingToId(null)
}
}}
onReply={(commentId) => {
if (isSignedIn) {
setReplyingToId(commentId)
}
}}
owner={owner}
reactions={reactions}
replyingToId={replyingToId}
repo={repo}
rootCommentId={rootCommentId}
/>
Expand Down
128 changes: 3 additions & 125 deletions apps/web/app/[owner]/[repo]/[postNumber]/comment-thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Link from "next/link"
import { useParams } from "next/navigation"
import { Suspense } from "react"
import type { AgentUIMessage } from "@/agent/types"
import type { ComposerProps } from "@/components/composer"
import { CopyLinkButton } from "@/components/copy-link-button"
import { CopyMarkdownButton } from "@/components/copy-markdown-button"
import { DeleteCommentButton } from "@/components/delete-comment-button"
Expand All @@ -15,10 +14,8 @@ import type {
mentions as mentionsSchema,
reactions as reactionsSchema,
} from "@/lib/db/schema"
import { cn } from "@/lib/utils"
import { CommentContent } from "./comment-content"
import { MentionBanner } from "./mention-banner"
import { PostComposer } from "./post-composer"
import {
StreamingBadge,
StreamingCommentProvider,
Expand All @@ -36,8 +33,6 @@ export type AuthorInfo = {
isLlm: boolean
}

const REPLIES_ENABLED = false

function CommentItem({
owner,
repo,
Expand All @@ -46,13 +41,6 @@ function CommentItem({
isRootComment,
author,
commentNumber,
depth = 0,
children,
hasReplies,
onReply,
onCancelReply,
isReplying,
askingOptions,
}: {
owner: string
repo: string
Expand All @@ -62,33 +50,19 @@ function CommentItem({
isRootComment: boolean
author: AuthorInfo
commentNumber: string
depth?: number
children?: React.ReactNode
hasReplies?: boolean
onReply?: (commentId: string) => void
onCancelReply?: () => void
isReplying?: boolean
askingOptions: ComposerProps["options"]["asking"]
}) {
const profileUrl = author.isLlm
? `/llm/${author.username}`
: `/user/${author.username}`

const canReply = REPLIES_ENABLED && depth === 0 && !isRootComment && onReply

const { postNumber } = useParams<{ postNumber: string }>()

const actionLabel = isRootComment ? "posted" : "commented"

const createdByLabel = comment.createdBy === "mcp" ? "via MCP" : ""

const header = (
<div
className={cn(
"z-10 flex items-center justify-between bg-shade px-2 py-1",
depth === 0 ? "sticky top-0" : "sticky top-8"
)}
>
<div className="sticky top-0 z-10 flex items-center justify-between bg-shade px-2 py-1">
<div className="flex flex-col sm:flex-row sm:items-center">
<Link
className="inline-flex items-center gap-2 font-semibold text-bright text-sm hover:underline"
Expand Down Expand Up @@ -173,47 +147,6 @@ function CommentItem({
body
)}
</div>

{hasReplies && (
<div className="my-4 flex items-center gap-3 text-faint text-xs">
<hr className="divider w-6" />
<span>REPLY IN THREAD</span>
<hr className="divider flex-1" />
</div>
)}

{children && (
<div className="border-muted border-l-2 pl-4">{children}</div>
)}

{canReply ? (
isReplying ? (
askingOptions ? (
<div className="mt-4 border-muted border-l-2 pl-4">
<PostComposer
askingOptions={askingOptions}
autoFocus
onCancel={onCancelReply}
owner={owner}
postId={comment.postId}
repo={repo}
storageKey={`reply:${comment.id}`}
threadCommentId={comment.id}
/>
</div>
) : null
) : (
<div className="mt-4 border-muted border-l-2 pl-4">
<button
className="text-muted-foreground text-sm hover:underline"
onClick={() => onReply(commentId)}
type="button"
>
Reply in thread
</button>
</div>
)
) : null}
</div>
)
}
Expand All @@ -231,10 +164,6 @@ export function CommentThread({
reactions,
rootCommentId,
commentNumbers,
replyingToId,
onReply,
onCancelReply,
askingOptions,
}: {
owner: string
repo: string
Expand All @@ -244,10 +173,6 @@ export function CommentThread({
reactions: Reaction[]
rootCommentId: string | null
commentNumbers: Map<string, string>
replyingToId?: string | null
onReply?: (commentId: string) => void
onCancelReply?: () => void
askingOptions: ComposerProps["options"]["asking"]
}) {
const reactionsByComment: Record<string, Reaction[]> = {}
for (const reaction of reactions) {
Expand All @@ -257,26 +182,15 @@ export function CommentThread({
reactionsByComment[reaction.commentId].push(reaction)
}

const topLevelComments = comments.filter((c) => c.threadCommentId === null)

const timeline: TimelineItem[] = [
...topLevelComments.map(
...comments.map(
(c) => ({ type: "comment", data: c, createdAt: c.createdAt }) as const
),
...mentions.map(
(m) => ({ type: "mention", data: m, createdAt: m.createdAt }) as const
),
].sort((a, b) => a.createdAt - b.createdAt)

const repliesByThread = new Map<string, Comment[]>()
for (const c of comments) {
if (c.threadCommentId) {
const existing = repliesByThread.get(c.threadCommentId) ?? []
existing.push(c)
repliesByThread.set(c.threadCommentId, existing)
}
}

return (
<div className="space-y-16">
{timeline.map((item) => {
Expand All @@ -296,54 +210,18 @@ export function CommentThread({
}
const commentNumber = commentNumbers.get(comment.id) ?? "?"
const isRootComment = comment.id === rootCommentId
const replies = repliesByThread.get(comment.id) ?? []
const hasReplies = replies.length > 0

return (
<CommentItem
askingOptions={askingOptions}
author={author}
comment={comment}
commentId={comment.id}
commentNumber={commentNumber}
depth={0}
hasReplies={hasReplies}
isReplying={replyingToId === comment.id}
isRootComment={isRootComment}
key={comment.id}
onCancelReply={onCancelReply}
onReply={onReply}
owner={owner}
reactions={reactionsByComment[comment.id] ?? []}
repo={repo}
>
{hasReplies && (
<div className="space-y-4">
{replies.map((reply) => {
const replyAuthor = authorsById[reply.authorId]
if (!replyAuthor) {
return null
}
const replyNumber = commentNumbers.get(reply.id) ?? "?"
return (
<CommentItem
askingOptions={askingOptions}
author={replyAuthor}
comment={reply}
commentId={reply.id}
commentNumber={replyNumber}
depth={1}
isRootComment={false}
key={reply.id}
owner={owner}
reactions={reactionsByComment[reply.id] ?? []}
repo={repo}
/>
)
})}
</div>
)}
</CommentItem>
/>
)
})}
</div>
Expand Down
1 change: 0 additions & 1 deletion apps/web/app/[owner]/[repo]/[postNumber]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,6 @@ export default async function PostPage({

<div className="mt-8 space-y-4">
<CommentThreadClient
askingOptions={askingOptions}
authorsById={authorsById}
commentNumbers={commentNumbers}
comments={postComments}
Expand Down
11 changes: 4 additions & 7 deletions apps/web/lib/actions/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,10 @@ export async function createComment(data: {
}
}

const isOp = session.user.id === post.authorId
const isTopLevel = !data.threadCommentId
// LLM replies to same thread as user (flat), or creates new thread if user is OP posting top-level
const llmThreadCommentId =
llm && !(isTopLevel && isOp)
? (data.threadCommentId ?? commentId)
: undefined
// Thread metadata remains supported, but the default comment flow is flat.
// If threaded replies are re-enabled, an explicit threadCommentId will keep
// the LLM response in that thread without auto-creating new threads.
const llmThreadCommentId = llm ? data.threadCommentId : undefined

let llmCommentId: string | undefined
let streamId: string | undefined
Expand Down
18 changes: 18 additions & 0 deletions apps/web/lib/utils/comment-numbers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test"
import { computeCommentNumbers } from "./comment-numbers"

describe("computeCommentNumbers", () => {
test("numbers comments flat, ignoring thread metadata", () => {
const numbers = computeCommentNumbers([
{ id: "root", threadCommentId: null, createdAt: 100 },
{ id: "comment", threadCommentId: null, createdAt: 200 },
{ id: "reply", threadCommentId: "comment", createdAt: 201 },
{ id: "next", threadCommentId: null, createdAt: 300 },
])

expect(numbers.get("root")).toBe("1")
expect(numbers.get("comment")).toBe("2")
expect(numbers.get("reply")).toBe("3")
expect(numbers.get("next")).toBe("4")
})
})
19 changes: 5 additions & 14 deletions apps/web/lib/utils/comment-numbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ type Comment = {
}

/**
* Computes hierarchical comment numbers from a flat list of comments.
* Computes flat comment numbers from a list of comments.
*
* Top-level comments: 1, 2, 3, 4...
* Replies in a thread: 3.1, 3.2, 3.3...
* Thread metadata is intentionally ignored while threaded replies are dormant,
* so every visible comment gets a top-level number: 1, 2, 3, 4...
*
* Numbers are deterministic based on createdAt order (with id as tiebreaker).
*/
Expand All @@ -23,18 +23,9 @@ export function computeCommentNumbers(
})

const result = new Map<string, string>()
const childCounters = new Map<string | null, number>()

for (const comment of sorted) {
const threadNumber = comment.threadCommentId
? result.get(comment.threadCommentId)
: null

const counter = (childCounters.get(comment.threadCommentId) ?? 0) + 1
childCounters.set(comment.threadCommentId, counter)

const number = threadNumber ? `${threadNumber}.${counter}` : String(counter)
result.set(comment.id, number)
for (const [index, comment] of sorted.entries()) {
result.set(comment.id, String(index + 1))
}

return result
Expand Down
Loading