mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 18:06:45 +08:00
bf1e375015f0bb6e2f736eaeb53ee1fa5cd84bdd
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f20999597 |
refactor(timeline): drop server-side comment + timeline pagination (#2322)
* refactor(timeline): drop server-side comment + timeline pagination (MUL-1929) The cursor-paginated /timeline and /comments endpoints were sized for a problem the data shape doesn't have: prod p99 is ~30 comments per issue and the all-time max is ~1.1k. Time-based pagination also splits reply threads across page boundaries (orphan replies), which the frontend was papering over with an "orphan rescue" that promoted disconnected replies to top-level — confusing UX with no real benefit. Replace both endpoints with a single full-issue fetch, capped server-side at 2000 rows as a defensive safety net (never hit in practice). Server - /api/issues/:id/timeline now returns a flat ASC TimelineEntry[] (matches the legacy desktop contract — older Multica.app builds keep working because the wrapped TimelineResponse + cursors are gone, and the raw array shape was always what they consumed). - /api/issues/:id/comments drops limit/offset; only ?since is honoured for the CLI agent-polling flow. - Drop ListCommentsBefore/After/Latest, ListActivitiesBefore/After/Latest and the timelineCursor encoding. - Replace with ListCommentsForIssue / ListCommentsSinceForIssue / ListActivitiesForIssue (capped by argument). CLI - multica issue comment list drops --limit / --offset and the X-Total-Count reporting; --since is preserved for incremental polling. Frontend - Replace useInfiniteQuery with useQuery in useIssueTimeline; drop fetchOlder/Newer, jumpToLatest, isAtLatest, newEntriesBelowCount. - Remove timeline-cache helpers (mapAllEntries / filterAllEntries / prependToLatestPage) and the TimelinePage / TimelinePageParam types. - WS event handlers update the single flat-array cache directly. - Drop the orphan-reply rescue in issue-detail — every reply's parent is now guaranteed to be in the same array. - Strip the "show older / show newer / jump to latest" buttons and their i18n strings. Co-authored-by: multica-agent <[email protected]> * fix(timeline): address review feedback on pagination removal Three issues caught in PR #2322 review: 1. /timeline broke for stale clients between #2128 and this PR. They send ?limit/?before/?after/?around and parse with the wrapped TimelinePageSchema; the new flat-array response was failing schema validation and falling back to an empty timeline. Restore the wrapped shape on those query params (DESC entries, null cursors, has_more_*=false), keeping the flat ASC array for bare requests. Around-mode now also fills target_index from the merged slice so legacy clients can still scroll-to-anchor without a follow-up. 2. The agent prompts in runtime_config.go and prompt.go still told agents that `multica issue comment list` accepts --limit/--offset and to use `--limit 30` on truncated output. With those flags removed in this PR, new agent runs would hit "unknown flag" or skip context. Update the prompt copy to "returns all comments, capped at 2000; --since for incremental polling". 3. useCreateComment's onSuccess was a bare append to the timeline cache with no id-dedupe, so a fast comment:created WS event firing before onSuccess produced a transient duplicate. Restore the id guard the old prependToLatestPage helper used to provide. Adds two new boundary tests: - TestListTimeline_LegacyWrappedShape_OnPaginationParams - TestListTimeline_LegacyWrappedShape_AroundFillsTargetIndex Co-authored-by: multica-agent <[email protected]> * test(handler): fix timeline test assertions for handler-package isolation The TestListTimeline_* assertions assumed CreateIssue would seed an "issue_created" activity_log row, but the activity listener that publishes those rows is registered in cmd/server/main.go — handler-package tests don't wire it up. CI saw 5 entries (3 comments + 2 activities) where the test expected ≥6. Drop the auto-activity assumption: assert exactly 5 entries in TestListTimeline_MergesCommentsAndActivities, and tighten TestListTimeline_EmptyIssue to assert a fully-empty timeline. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
3b3be9d7bd |
feat(comments): resolve threads with collapsible bar (MUL-1895) (#2300)
* feat(comments): resolve threads with collapsible bar (MUL-1895)
Adds a Linear-style resolve action on comment thread roots. Resolved
threads collapse to a single "N resolved comments from X" bar in the
activity feed; clicking expands the thread inline (per-session, not
persisted). Replying inside a resolved thread auto-unresolves it.
Backend
- migration 069: resolved_at, resolved_by_type, resolved_by_id on comment
- sqlc ResolveComment / UnresolveComment queries (idempotent via COALESCE)
- POST/DELETE /api/comments/{id}/resolve handlers, root-only validation
- CreateComment auto-clears resolved_at when a reply lands in a resolved
thread, publishing comment:unresolved
- comment:resolved / comment:unresolved events; CommentResponse and
TimelineEntry both surface the new fields
Frontend
- Comment + TimelineEntry types extended; payloads typed; WS sync wired
- useResolveComment optimistic mutation with rollback
- ResolvedThreadBar component for the collapsed view
- Resolve / Unresolve menu items on root comments; Collapse strip on the
expanded resolved card
- en + zh-Hans locale strings
Co-authored-by: multica-agent <[email protected]>
* fix(comments): cover agent reply path, expand-state hygiene, nested counts (MUL-1895)
Addresses three review issues from Emacs on PR #2300:
1. TaskService.createAgentComment bypasses Handler.CreateComment, so the
auto-unresolve wired into the handler did not fire when an agent replied
in a resolved thread (task / mention / on_comment paths). Extracted the
logic to TaskService.AutoUnresolveThreadOnReply so both reply paths share
it; rewired Handler.CreateComment to call the new method.
2. Resolving an already-expanded thread no longer collapses it back to the
bar because expandedResolved still contained the id. Added
clearResolvedExpand + handleResolveToggle wrapper so resolve / unresolve
always wipe the session expand entry.
3. ResolvedThreadBar received only direct children, while CommentCard's
expanded view recurses through descendants. Extracted the recursive
walk into thread-utils.collectThreadReplies and called from both —
counts and author lists now match.
Co-authored-by: multica-agent <[email protected]>
* test(comments): mock useResolveComment + add zh-Hans plural key
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
b1d874ef50 |
fix(timeline): rescue orphaned replies + bump page size to 50 (#2263)
Two related changes for the same UX problem (#1857 follow-up). 1. Orphan-reply rescue. The grouping in issue-detail.tsx put replies under their parent's CommentCard, looking them up via repliesByParent.get(parentId). When a reply's parent wasn't in the loaded timeline — pagination boundary, merge truncation, future backend bug — the entire reply subtree dropped off the screen, since the orphan replies sat in the map with no CommentCard around to render them. MUL-1847 hit this on the OLD backend: 1 root + 29 replies, the root was the oldest entry and the merge dropped it, so all 29 replies vanished from the UI even though the API returned them. The fix: a reply whose parent_id points to a comment NOT in the loaded timeline is promoted to top-level. It still loses its visual indentation under the missing parent, but it stops disappearing. 2. Page size 50. With activities now decoupled from the comment budget (#2253) and the off-by-one fixed (#2259), 50 fits the typical issue without any "Show older" interaction. Cost is bounded — SQL fetches limit+1 = 51 comments + 50 activities through the keyset index from migration 068; response body grows ~70% over 30 but stays well under the legacy compat path's 200-row cap. UI renders 100 entries comfortably; CommentCards memoize. Frontend default in `client.ts` (`limit = 50`) matches the new backend default (`timelineDefaultLimit = 50`) so pages walk consistently. Test: render-level case in `issue-detail.test.tsx` mocks a timeline page containing only an orphaned reply (parent_id refers to a missing id) and asserts the reply text appears. Co-authored-by: multica-agent <[email protected]> |
||
|
|
6400868412 |
fix(timeline): off-by-one — exact-limit comments no longer triggers Show older (#2259)
Pre-fix the gate was `len(comments) >= limit`, which fired even when the issue had EXACTLY <limit> comments. The "Show older" affordance appeared, the user clicked, the next page fetched zero rows. User flagged it on MUL-1857 — "this issue happens to have 30 comments; the button shouldn't appear in that case." The fix is the standard over-fetch probe: ask the SQL for limit+1 rows; if it returned more than limit, drop the extra and report hasMore=true. Otherwise hasMore=false. - New helper `commentOverflow(rows, limit) -> ([]db.Comment, bool)` replaces the count-based `hasMoreCommentsBeyond`. Works for both DESC (latest / before) and ASC (after / around-newer) since both want "keep first <limit>". - All four mode handlers (latest, before, after, around) now ask for limit+1 comments and route through the helper. - Activities still cap at <limit> with no overflow probe — they don't gate pagination (#1857), so the boundary doesn't matter for them. Tests: - TestCommentOverflow pins the truth table with the boundary case ("exactly limit comments" → hasMore=false). - TestListTimeline_ExactlyLimitCommentsHidesShowOlder is the DB-backed regression: 30 comments, limit=30, asserts has_more_before=false and next_cursor=nil. Co-authored-by: multica-agent <[email protected]> |
||
|
|
161194b86f |
fix(timeline): exclude activities from comment page budget (#2253)
* fix(timeline): exclude activities from comment page budget The /timeline endpoint paginated comments + activities through one shared 50-row budget, so an issue with a chatty agent (status flips, task_completed markers, assignee toggles per run) could trigger "show older" with as few as 10-20 actual comments — users opened the page and thought their discussion had vanished. - Comment limit drops from 50 to 30 (the visible page size users wanted). - has_more_before / has_more_after gate on comments alone via the new hasMoreCommentsBeyond helper. Activity rows still ride along at the same per-call SQL cap but no longer push real comments off-page. - Merge functions stop truncating at the page limit; both pools are individually bounded by SQL, so dropping rows here only re-introduced the bug. The legacy (pre-cursor) path applies its 200-row cap inline. - Test rewrite: TestHasMoreBeyond → TestHasMoreCommentsBeyond, replaced the #2192 merge-truncation regression with a #1857 "dense activity does not hide comments" test that pins the new contract directly. Co-authored-by: multica-agent <[email protected]> * fix(timeline): per-pool keyset cursor for comments and activities Pre-fix, next_cursor / prev_cursor anchored on the merged page boundary (oldest / newest entry overall). When activity rows were older than every fetched comment — common on issues created with a status change before the first comment — the latest page emitted a cursor pointing at that activity, and the next "show older" call sent that timestamp into ListCommentsBefore, skipping every unreturned comment in between. GPT-Boy flagged this on PR #2253 with the 80-comment / 30-activity scenario where 50 comments became permanently unreachable. The fix splits the cursor into independent comment and activity positions: - timelineCursor carries (CommentT, CommentID, ActivityT, ActivityID). encode/decode signatures changed accordingly. - New cursorPos type and four bounds helpers (commentBoundsDesc / Asc, activityBoundsDesc / Asc) extract per-pool oldest/newest from fetched rows, with a carry fallback so empty pools advance past the input cursor instead of resetting. - All four mode handlers (latest, before, after, around) now derive cursors from each pool's own bounds. Removed the entryTimestamp / entryID helpers that re-parsed the merged entry slice. Tests: - TestTimelineCursor_RoundTrip pins the encode/decode contract for the new dual-pool format (and rejects garbage input). - TestListTimeline_PerPoolCursorWalksAllComments reproduces GPT-Boy's exact scenario (30 activities older than 80 comments, limit=30) and asserts every comment is reachable through repeated `before=<cursor>` walks. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> |
||
|
|
099dda0603 |
fix(timeline): include merge-truncation case in has_more_before (#2192) (#2204)
* fix(timeline): include merge-truncation case in has_more_before (#2192) Older comments became unreachable on issues where activity-log entries crowded them out of the latest 50-entry page. The 'show earlier' button was hidden and no cursor was emitted because the has_more_before formula only caught the per-table SQL cap case and missed the in-memory merge truncation case. Reproduces with 48 comments + 49 activities, default limit 50: neither table individually returns >= limit rows, but their sum (97) exceeds the merged page size, so the merge silently drops 47 older comments. The old formula reported has_more_before=false; the client never asked for page 2. Fix: extract hasMoreBeyond(c, a, e, limit) with the missing third disjunct - comments + activities > entries - applied uniformly to listTimelineLatest / Before / After / Around. Backwards compatible: API contract unchanged. Pre-cursor clients (<=v0.2.25) still hit listTimelineLegacy and never read these fields. Newer clients see has_more_before flip from 'wrongly false' to correctly true/false - no field renames, no shape changes. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(issues): show count badge when activities are coalesced (#2192) The timeline coalesces consecutive same-actor + same-action activities within a 2-minute window so 48 status_changed entries don't take 48 rows. The count badge was only rendered for task_completed / task_failed; for status_changed (and every other action) the coalesced batch silently collapsed to a single line with no hint that N entries were merged. Add a coalesced_badge translation and render '×N' next to the activity text whenever coalesced_count > 1, suppressing it on task_completed / task_failed which already include the count in their translation copy. This pairs with the backend fix for #2192: once the older-comments page becomes reachable again, the activity rows above it should make the density of the merged batch visible rather than misleading the user into thinking only one event happened. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> |
||
|
|
11a6288cbd |
fix(timeline): legacy array shape for pre-#2128 clients (#2143, #2147) (#2156)
#2128 changed GET /api/issues/:id/timeline from a bare TimelineEntry[] to a wrapped { entries, next_cursor, ... } object. Multica.app ≤ v0.2.25 still in the wild reads the response body as TimelineEntry[] directly, so the moment v0.2.26 backend rolled out, every old desktop hit "timeline.filter is not a function" on any issue open — bug reports landed within ten minutes of the v0.2.26 release (#2143, #2147). The new client always sends ?limit=..., so absence of every pagination param uniquely identifies a legacy caller. Detect that at the top of ListTimeline and serve the old shape (ASC, []TimelineEntry, capped at 200) through a dedicated listTimelineLegacy helper. New clients fall through unchanged. A new TestListTimeline_LegacyShapeForPreCursorClients pins the contract (array shape, ASC order, "[]" not "null" on empty issues). Two existing tests that used the empty query string have been updated to send ?limit=50, since the empty form is now reserved for the compat path. The legacy branch can be deleted once desktop auto-update has rolled the user base past v0.2.26. Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> |
||
|
|
ba147708a6 |
fix(timeline): cursor-paginated timeline to stop long-issue freeze (#1968) (#2128)
* 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) <[email protected]> * 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) <[email protected]> --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> |
||
|
|
f0f3cb5c3a |
fix(server): resolve X-Workspace-Slug in middleware-less handlers (#1165)
Problem ------- The v2 workspace URL refactor (#1141) switched the frontend from sending X-Workspace-ID (UUID) to X-Workspace-Slug. The workspace middleware was updated to accept the slug and translate it via GetWorkspaceBySlug. But the handler package maintained a PARALLEL resolver (`resolveWorkspaceID` in handler.go) used by endpoints that sit outside the workspace middleware — and that resolver was never updated. It only checked context / ?workspace_id / X-Workspace-ID, never the slug. /api/upload-file is the one production route that hit the broken path: it's user-scoped (not behind workspace middleware) because it also serves avatar uploads (no workspace). Post-refactor requests from the frontend arrived with only X-Workspace-Slug; the handler resolver returned "", the code fell into the "no workspace context" branch, and every file upload since v2 landed in S3 with no corresponding DB attachment row — files orphaned, invisible to the UI. Root cause is structural: two resolvers doing the same job, written independently, diverged silently when one was updated. Fix --- Collapse to a single shared helper. middleware.ResolveWorkspaceIDFromRequest is the new canonical resolver; both the middleware's internal `resolveWorkspaceUUID` (for middleware gating) and the handler-side `(h *Handler).resolveWorkspaceID` (promoted from a package function) now delegate to it. Priority order matches what the middleware has had since v2: context > X-Workspace-Slug header > ?workspace_slug query > X-Workspace-ID header > ?workspace_id query. Impact analysis --------------- 47 call sites of the old `resolveWorkspaceID(r)` are renamed to `h.resolveWorkspaceID(r)`. 46 of them sit behind workspace middleware, so they hit the context fast path and see zero behavior change. The one caller that actually gains capability is UploadFile — which now correctly recognizes slug requests and creates DB attachment rows. Tests ----- - New table-driven unit test for ResolveWorkspaceIDFromRequest covers all priority levels and the unknown-slug fallback. - Regression tests for UploadFile: once with X-Workspace-Slug only (the broken path), once with X-Workspace-ID only (legacy CLI/daemon compat path). Both assert that a DB attachment row is created. - Full Go test suite passes; typecheck + pnpm test unaffected. Plan ---- See docs/plans/2026-04-16-unify-workspace-identity-resolver.md for the full first-principles writeup. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
cce210ed3a |
feat(assign): sort members & agents by user's assignment frequency (#652)
The Assign dropdown now sorts members and agents by how frequently the current user assigns issues to them. Frequency is computed from two sources: assignee_changed activities in the activity log and initial assignments on issues created by the user. Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
f5353c6691 |
feat(upload): signed URLs for CLI + eager load attachments on comments
- Add CloudFrontSigner.SignedURL() for generating per-resource signed URLs - Attachment responses include download_url (5-min signed URL for CLI) - Eager load attachments on comments and timeline (same pattern as reactions) - Add ListAttachmentsByCommentIDs query for batch loading - Update Comment and TimelineEntry types with attachments field Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
7c1aabbe3a |
feat(reactions): add emoji reactions for comments and issue descriptions
Add Slack-style emoji reactions to comments and issue descriptions with full-stack support: database tables, REST API endpoints, real-time WebSocket sync, optimistic UI updates, and inbox notifications. - New `comment_reaction` and `issue_reaction` tables with migrations - POST/DELETE endpoints for adding/removing reactions on both comments and issue descriptions - Real-time WS events (reaction:added/removed, issue_reaction:added/removed) - Shared ReactionBar component with quick emoji picker and full emoji-mart picker (lazy-loaded) - Optimistic add/remove with rollback on failure - Inbox notifications for comment author and issue creator when reacted to - Reactions included in timeline, comment list, and issue detail responses |
||
|
|
9ede795c5b |
feat(api): strict workspace isolation + agent parity fixes
Enforce workspace isolation at every layer: - Router: move RequireWorkspaceMember middleware to group level so ALL workspace-scoped routes (issues, agents, skills, runtimes, inbox, comments) require workspace context - SQL: add GetXxxInWorkspace queries that filter by workspace_id, eliminating cross-workspace data access at the query level - Handlers: loadXForUser functions use workspace-scoped queries, no fallback to unscoped queries - Migration 025: add workspace_id column to comment table with backfill - ListComments: add workspace_id filter for defense-in-depth Fix daemon workspace mapping: - Server returns workspace_id in task claim response (from issue) - Daemon uses task.WorkspaceID directly instead of unreliable workspaceIDForRuntime() local map lookup - Remove workspaceIDForRuntime function Fix agent/human parity: - Comment update/delete: use resolveActor for isAuthor check so agents can edit/delete their own comments - Event attribution: replace hardcoded "member" with resolveActor in agent, skill, and subscriber publish calls Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> |
||
|
|
e7fe6ea79b |
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) <[email protected]>
|