feat(activity): unified activity timeline with comment reply support

Replace the comment-only list with a Linear-style unified timeline that
interleaves field changes and comments chronologically.

Backend:
- activity_listeners.go: records field changes (status, assignee, description,
  task completed/failed) to activity_log table on domain events
- Timeline API: GET /api/issues/{id}/timeline merges activity_log + comments
  sorted by created_at
- Comment reply: parent_id column + handler support for threading

Frontend:
- Unified timeline replaces comment list: activity entries as compact muted
  lines, comments as Card components with reply threading
- Filter toggle (All / Comments / Activity)
- Reply UI: inline editor under comments with Cancel/Reply buttons
- Real-time sync for activity:created + comment events
- 10 new Go tests, all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-28 21:53:08 +08:00
parent 3bb79564ed
commit e7fe6ea79b
21 changed files with 1307 additions and 132 deletions

View file

@ -30,6 +30,7 @@ import type {
CreatePersonalAccessTokenResponse,
RuntimeUsage,
RuntimePing,
TimelineEntry,
} from "@/shared/types";
import { type Logger, noopLogger } from "@/shared/logger";
@ -183,13 +184,21 @@ export class ApiClient {
return this.fetch(`/api/issues/${issueId}/comments`);
}
async createComment(issueId: string, content: string, type?: string): Promise<Comment> {
async createComment(issueId: string, content: string, type?: string, parentId?: string): Promise<Comment> {
return this.fetch(`/api/issues/${issueId}/comments`, {
method: "POST",
body: JSON.stringify({ content, type: type ?? "comment" }),
body: JSON.stringify({
content,
type: type ?? "comment",
...(parentId ? { parent_id: parentId } : {}),
}),
});
}
async listTimeline(issueId: string): Promise<TimelineEntry[]> {
return this.fetch(`/api/issues/${issueId}/timeline`);
}
async updateComment(commentId: string, content: string): Promise<Comment> {
return this.fetch(`/api/comments/${commentId}`, {
method: "PUT",