mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 13:49:18 +02:00
* fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) Opening an issue from Inbox with thousands of timeline entries used to hard-freeze the browser tab on a synchronous render of every comment + activity. The whole pipeline was unbounded: the API returned every row, TanStack Query cached the full array, and IssueDetail mounted N CommentCards (each running a full react-markdown + lowlight pipeline) in one frame. This swaps the timeline endpoint to keyset cursor pagination and rewires the frontend to useInfiniteQuery so a long issue costs the same as a short one on first paint. API: - GET /issues/:id/timeline now accepts ?before / ?after / ?around (mutex) + ?limit (default 50, max 100); response wraps entries with next/prev cursors and has_more flags. Cursors are opaque base64 (created_at, id). - ?around=<entry_id> anchors a window on the target so Inbox notifications pointing at an old comment never trigger the freeze. - New composite indexes on (issue_id, created_at DESC, id DESC) replace the redundant single-column ones so keyset queries are index-only scans. - /issues/:id/comments default branch now caps at 50 instead of returning every row unbounded; the unbounded ListComments / ListActivities sqlc queries are deleted. Frontend: - useIssueTimeline switches to useInfiniteQuery, exposes fetchOlder/fetchNewer/jumpToLatest + isAtLatest + newEntriesBelowCount. - WS handlers respect the at-latest invariant: comment/activity:created prepends to pages[0] only when the user is reading the live tail; otherwise it just bumps a counter so the UI offers a "Jump to latest" affordance without yanking scroll. - Optimistic mutations adapted to the InfiniteData shape via shared helpers (mapAllEntries / filterAllEntries / prependToLatestPage in core/issues/timeline-cache.ts) and use setQueriesData so all open windows of the same issue stay in sync. - IssueDetail Activity section gets a TimelineSkeleton placeholder during the brief load window plus subtle text-link load-more buttons matching the existing Subscribe affordance (no Button chrome). Top uses a divider for boundary clarity; bottom shows "Jump to latest · N new" weighted slightly heavier when there's unread state. - highlightCommentId now flows into the hook's around parameter so Inbox jumps fetch the surrounding 50 entries directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(agent): default comment list to 50 + prompt hint about long issues The CLI's "multica issue comment list" used to default to --limit 0 (meaning "fetch every comment"), which lets an agent on a long issue fill its context window with thousands of rows. The default is now 50; agents that need older history can pass --limit or --since explicitly. The local-coding-agent prompt also gains a single-line note about this in both the comment-triggered and on-assign flows so the agent knows to scope its fetches when issue size is unknown. Autopilot run-only mode is intentionally unchanged — it has no issue context to query. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.6 KiB
SQL
87 lines
2.6 KiB
SQL
-- name: ListCommentsPaginated :many
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2
|
|
ORDER BY created_at ASC
|
|
LIMIT $3 OFFSET $4;
|
|
|
|
-- name: ListCommentsSince :many
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2 AND created_at > $3
|
|
ORDER BY created_at ASC;
|
|
|
|
-- name: ListCommentsSincePaginated :many
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2 AND created_at > $3
|
|
ORDER BY created_at ASC
|
|
LIMIT $4 OFFSET $5;
|
|
|
|
-- name: ListCommentsLatest :many
|
|
-- Top N comments for an issue, newest first. Backs the default cursor
|
|
-- pagination entry point (no cursor → return the most recent page).
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT $3;
|
|
|
|
-- name: ListCommentsBefore :many
|
|
-- Keyset pagination: comments older than ($3, $4) tuple. Returns DESC so the
|
|
-- caller can stitch pages without re-sorting.
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2
|
|
AND (created_at, id) < ($3::timestamptz, $4::uuid)
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT $5;
|
|
|
|
-- name: ListCommentsAfter :many
|
|
-- Keyset pagination: comments newer than ($3, $4) tuple. Returns ASC because
|
|
-- "newer" pagination naturally walks forward in time; the merge layer
|
|
-- normalizes to the response order.
|
|
SELECT * FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2
|
|
AND (created_at, id) > ($3::timestamptz, $4::uuid)
|
|
ORDER BY created_at ASC, id ASC
|
|
LIMIT $5;
|
|
|
|
-- name: CountComments :one
|
|
SELECT count(*) FROM comment
|
|
WHERE issue_id = $1 AND workspace_id = $2;
|
|
|
|
-- name: GetComment :one
|
|
SELECT * FROM comment
|
|
WHERE id = $1;
|
|
|
|
-- name: GetCommentInWorkspace :one
|
|
SELECT * FROM comment
|
|
WHERE id = $1 AND workspace_id = $2;
|
|
|
|
-- name: CreateComment :one
|
|
INSERT INTO comment (issue_id, workspace_id, author_type, author_id, content, type, parent_id)
|
|
VALUES ($1, $2, $3, $4, $5, $6, sqlc.narg(parent_id))
|
|
RETURNING *;
|
|
|
|
-- name: UpdateComment :one
|
|
UPDATE comment SET
|
|
content = $2,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: HasAgentCommentedSince :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM comment
|
|
WHERE issue_id = @issue_id
|
|
AND author_type = 'agent'
|
|
AND author_id = @author_id
|
|
AND created_at >= @since
|
|
) AS commented;
|
|
|
|
-- name: HasAgentRepliedInThread :one
|
|
-- Returns true if the given agent has posted a reply in the thread rooted at
|
|
-- the specified parent comment. Used to detect agent participation in a
|
|
-- member-started thread so that follow-up member replies still trigger the agent.
|
|
SELECT count(*) > 0 AS has_replied FROM comment
|
|
WHERE parent_id = @parent_id AND author_type = 'agent' AND author_id = @agent_id;
|
|
|
|
-- name: DeleteComment :exec
|
|
DELETE FROM comment WHERE id = $1;
|