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:
parent
3bb79564ed
commit
e7fe6ea79b
21 changed files with 1307 additions and 132 deletions
|
|
@ -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",
|
||||
|
|
|
|||
15
apps/web/shared/types/activity.ts
Normal file
15
apps/web/shared/types/activity.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export interface TimelineEntry {
|
||||
type: "activity" | "comment";
|
||||
id: string;
|
||||
actor_type: string;
|
||||
actor_id: string;
|
||||
created_at: string;
|
||||
// Activity fields
|
||||
action?: string;
|
||||
details?: Record<string, unknown>;
|
||||
// Comment fields
|
||||
content?: string;
|
||||
parent_id?: string | null;
|
||||
updated_at?: string;
|
||||
comment_type?: string;
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ export interface Comment {
|
|||
author_id: string;
|
||||
content: string;
|
||||
type: CommentType;
|
||||
parent_id: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Issue } from "./issue";
|
|||
import type { Agent } from "./agent";
|
||||
import type { InboxItem } from "./inbox";
|
||||
import type { Comment } from "./comment";
|
||||
import type { TimelineEntry } from "./activity";
|
||||
import type { Workspace, MemberWithUser } from "./workspace";
|
||||
|
||||
// WebSocket event types (matching Go server protocol/events.go)
|
||||
|
|
@ -35,7 +36,8 @@ export type WSEventType =
|
|||
| "skill:updated"
|
||||
| "skill:deleted"
|
||||
| "subscriber:added"
|
||||
| "subscriber:removed";
|
||||
| "subscriber:removed"
|
||||
| "activity:created";
|
||||
|
||||
export interface WSMessage<T = unknown> {
|
||||
type: WSEventType;
|
||||
|
|
@ -139,3 +141,8 @@ export interface SubscriberRemovedPayload {
|
|||
user_type: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export interface ActivityCreatedPayload {
|
||||
issue_id: string;
|
||||
entry: TimelineEntry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export type {
|
|||
export type { Workspace, WorkspaceRepo, Member, MemberRole, User, MemberWithUser } from "./workspace";
|
||||
export type { InboxItem, InboxSeverity, InboxItemType } from "./inbox";
|
||||
export type { Comment, CommentType, CommentAuthorType } from "./comment";
|
||||
export type { TimelineEntry } from "./activity";
|
||||
export type { IssueSubscriber } from "./subscriber";
|
||||
export type { DaemonPairingSession, DaemonPairingSessionStatus, ApproveDaemonPairingSessionRequest } from "./daemon";
|
||||
export type * from "./events";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue