mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 22:10:25 +00:00
2f3cf402b1
* fix(timeline): 'commented on update' only for a comment an update carried (BUG-3437) TimelineCommentCard showed 'commented on update' whenever the comment had an activity_id, and every comment has one: a comment or reply written on its own links to its own 'commented' activity, so a plain 'pad item comment' read as if it were attached to a change. The store's comment list join (the one that already reads the agent name off the linked activity) now also reads its action into Comment.LinkedActivityAction (json:"-"), and the timeline sets an additive, omitempty TimelineEntry.comment_on_update when that action is anything but 'commented'. The card keys the label on it. The comments API, the CLI and MCP keep their shape (the new field is never serialized outside the timeline, which MCP does not expose). Tests: the server test writes a standalone comment, a reply and an update carrying a comment through the real routes (web, CLI and MCP share the comments route) and checks the flag, with the old rule and an always-false mutant each red; the web test pins the card and the entry list's pass-through, each red with its half reverted. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * test(timeline): a comment that arrives live keeps 'commented on update' (BUG-3437) The label comes from the server-set comment_on_update. Comments that arrive while the timeline is open come through the SSE refresh, which re-reads the timeline endpoint (nothing builds a comment entry client-side), so they carry the flag. This pins it: red if the entry list stops passing the flag. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
116 lines
5.1 KiB
Go
116 lines
5.1 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// Comment represents a comment on an item.
|
|
type Comment struct {
|
|
ID string `json:"id"`
|
|
ItemID string `json:"item_id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Author string `json:"author"`
|
|
// UserID is the authenticated user who authored the comment. Empty for
|
|
// pre-identity comments (created before TASK-1663) and agent/system
|
|
// comments; the comment-edit permission check treats empty as
|
|
// "no provable author" → admin-only.
|
|
UserID string `json:"user_id,omitempty"`
|
|
Body string `json:"body"`
|
|
CreatedBy string `json:"created_by"`
|
|
Source string `json:"source"`
|
|
ActivityID string `json:"activity_id,omitempty"`
|
|
ParentID string `json:"parent_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
// Deleted marks a tombstone: a comment deleted while it still had
|
|
// replies, kept so they keep their parent (BUG-3252). Its body is empty;
|
|
// author and timestamps are the original ones.
|
|
Deleted bool `json:"deleted,omitempty"`
|
|
// Imported marks a comment a workspace import wrote (BUG-3379): its
|
|
// author string came from the export and was never verified against an
|
|
// account here. Set only by the import door.
|
|
Imported bool `json:"imported,omitempty"`
|
|
|
|
// Populated by joins (not stored)
|
|
//
|
|
// LinkedActivityAction is the action of the activity ActivityID points at,
|
|
// read by the same list join as AgentName (BUG-3437): "commented" for a
|
|
// comment or reply on its own, "updated" (or another change) for a comment
|
|
// an item update carried. Never serialized: the item timeline turns it into
|
|
// TimelineEntry.CommentOnUpdate, and nothing else needs it, so the
|
|
// comments API and MCP keep their shape. Empty on a comment read any other
|
|
// way, and on one with no linked activity.
|
|
LinkedActivityAction string `json:"-"`
|
|
ItemTitle string `json:"item_title,omitempty"`
|
|
ItemSlug string `json:"item_slug,omitempty"`
|
|
// AgentName is the display name the writing agent declared (the
|
|
// X-Pad-Agent header), read off the activity this comment's ActivityID
|
|
// points at — the `commented` row a comment or reply logs, or the
|
|
// `updated` row of an item update that carried the comment. Comments
|
|
// themselves never store it; workspace activity rows are the only
|
|
// carrier (TASK-2759 / TASK-2760). Its ONLY
|
|
// writer is the LEFT JOIN in the store's list queries (scanComments in
|
|
// comments.go), so it is empty on a comment read any other way
|
|
// (GetComment, the create/update read-back) and on a comment whose
|
|
// activity carries no stamp — a human's write, a pre-BUG-2542 row, an
|
|
// agent that sent no header. It is populated whatever CreatedBy says;
|
|
// the client decides whether the actor kind makes it meaningful.
|
|
// Self-declared, so it records what the client said, not who acted.
|
|
AgentName string `json:"agent_name,omitempty"`
|
|
|
|
// Populated by handlers for threaded views
|
|
Replies []Comment `json:"replies,omitempty"`
|
|
Reactions []Reaction `json:"reactions,omitempty"`
|
|
|
|
// ViaApp and ViaAppName name the installed app that wrote the comment,
|
|
// absent for a person's or agent's (SPEC-6 U9c, TASK-3413). The name
|
|
// is the app's bot display name, else its origin, and survives
|
|
// uninstall. The server fills both on read; no scan sets them.
|
|
ViaApp string `json:"via_app,omitempty"`
|
|
ViaAppName string `json:"via_app_name,omitempty"`
|
|
}
|
|
|
|
// IsEdited reports whether the body changed after creation: create stamps
|
|
// created_at and updated_at with one value, and only an edit moves updated_at
|
|
// (reactions live in their own table). Timestamps are second-precision, so an
|
|
// edit inside the creation second is not seen. Same rule as the web timeline's
|
|
// isEdited (TimelineCommentCard.svelte); TASK-2695 brought it to the CLI.
|
|
func (c Comment) IsEdited() bool {
|
|
return !c.CreatedAt.IsZero() && c.UpdatedAt.After(c.CreatedAt)
|
|
}
|
|
|
|
// MarshalJSON adds a derived, read-only `edited` (IsEdited) to every
|
|
// serialised comment, so an agent reading JSON on either MCP transport sees
|
|
// the same marker the CLI table and the web timeline show without comparing
|
|
// timestamps itself (TASK-2695). It is not stored and not read back: Comment
|
|
// has no field for it, so a decode ignores it.
|
|
func (c Comment) MarshalJSON() ([]byte, error) {
|
|
type plain Comment
|
|
return json.Marshal(struct {
|
|
plain
|
|
Edited bool `json:"edited"`
|
|
}{plain(c), c.IsEdited()})
|
|
}
|
|
|
|
// CommentCreate is the input for creating a new comment.
|
|
type CommentCreate struct {
|
|
Author string `json:"author,omitempty"`
|
|
Body string `json:"body"`
|
|
CreatedBy string `json:"created_by,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
ParentID string `json:"parent_id,omitempty"`
|
|
ActivityID string `json:"activity_id,omitempty"`
|
|
}
|
|
|
|
// Reaction represents an emoji reaction on a comment.
|
|
type Reaction struct {
|
|
ID string `json:"id"`
|
|
CommentID string `json:"comment_id"`
|
|
UserID string `json:"user_id,omitempty"`
|
|
Actor string `json:"actor"`
|
|
Emoji string `json:"emoji"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ActorName string `json:"actor_name,omitempty"`
|
|
}
|