mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-26 08:59:31 +02:00
Compare commits
1 Commits
fix/settin
...
codex/comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa312c3789 |
@@ -265,6 +265,7 @@ function CommentRow({
|
||||
const isOwn = entry.actor_type === "member" && entry.actor_id === currentUserId;
|
||||
const canEditEntry = isOwn || (canModerate && entry.actor_type === "member");
|
||||
const canDeleteEntry = isOwn || canModerate;
|
||||
const isTemp = entry.id.startsWith("temp-");
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
const startEdit = () => {
|
||||
@@ -324,7 +325,7 @@ function CommentRow({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="py-3">
|
||||
<div className={`py-3${isTemp ? " opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<ActorAvatar actorType={entry.actor_type} actorId={entry.actor_id} size={24} enableHoverCard showStatusDot />
|
||||
<span className="cursor-pointer text-sm font-medium">
|
||||
@@ -343,11 +344,12 @@ function CommentRow({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
<QuickEmojiPicker
|
||||
onSelect={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
align="end"
|
||||
/>
|
||||
{!isTemp && (
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
<QuickEmojiPicker
|
||||
onSelect={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
align="end"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
@@ -389,7 +391,8 @@ function CommentRow({
|
||||
onOpenChange={setConfirmDelete}
|
||||
onConfirm={() => onDelete(entry.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
@@ -448,14 +451,16 @@ function CommentRow({
|
||||
<ReadonlyContent content={entry.content ?? ""} attachments={entry.attachments} />
|
||||
</div>
|
||||
<AttachmentList attachments={entry.attachments} content={entry.content} className="mt-1.5 pl-8" />
|
||||
<ReactionBar
|
||||
reactions={reactions}
|
||||
currentUserId={currentUserId}
|
||||
onToggle={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
getActorName={getActorName}
|
||||
hideAddButton={!isLongContent}
|
||||
className="mt-1.5 pl-8"
|
||||
/>
|
||||
{!isTemp && (
|
||||
<ReactionBar
|
||||
reactions={reactions}
|
||||
currentUserId={currentUserId}
|
||||
onToggle={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
getActorName={getActorName}
|
||||
hideAddButton={!isLongContent}
|
||||
className="mt-1.5 pl-8"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -523,6 +528,7 @@ function CommentCardImpl({
|
||||
// own their own outputs.
|
||||
const canEditEntry = isOwn || (canModerate && entry.actor_type === "member");
|
||||
const canDeleteEntry = isOwn || canModerate;
|
||||
const isTemp = entry.id.startsWith("temp-");
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
const startEdit = () => {
|
||||
@@ -591,7 +597,7 @@ function CommentCardImpl({
|
||||
const isHighlighted = highlightedCommentId === entry.id;
|
||||
|
||||
return (
|
||||
<Card className={cn("!py-0 !gap-0 overflow-hidden transition-colors duration-700", isHighlighted && "ring-2 ring-brand/50 bg-brand/5")}>
|
||||
<Card className={cn("!py-0 !gap-0 overflow-hidden transition-colors duration-700", isTemp && "opacity-60", isHighlighted && "ring-2 ring-brand/50 bg-brand/5")}>
|
||||
{onCollapseResolved && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -641,7 +647,7 @@ function CommentCardImpl({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
{open && !isTemp && (
|
||||
<div className="ml-auto flex items-center gap-0.5">
|
||||
<QuickEmojiPicker
|
||||
onSelect={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
@@ -772,14 +778,16 @@ function CommentCardImpl({
|
||||
<ReadonlyContent content={entry.content ?? ""} attachments={entry.attachments} />
|
||||
</div>
|
||||
<AttachmentList attachments={entry.attachments} content={entry.content} className="mt-1.5 pl-10" />
|
||||
<ReactionBar
|
||||
reactions={reactions}
|
||||
currentUserId={currentUserId}
|
||||
onToggle={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
getActorName={getActorName}
|
||||
hideAddButton={!isLongContent}
|
||||
className="mt-1.5 pl-10"
|
||||
/>
|
||||
{!isTemp && (
|
||||
<ReactionBar
|
||||
reactions={reactions}
|
||||
currentUserId={currentUserId}
|
||||
onToggle={(emoji) => onToggleReaction(entry.id, emoji)}
|
||||
getActorName={getActorName}
|
||||
hideAddButton={!isLongContent}
|
||||
className="mt-1.5 pl-10"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -77,10 +77,10 @@ function CommentInput({ issueId, onSubmit }: CommentInputProps) {
|
||||
.filter((a) => content.includes(a.url))
|
||||
.map((a) => a.id);
|
||||
setSubmitting(true);
|
||||
editorRef.current?.clearContent();
|
||||
setIsEmpty(true);
|
||||
try {
|
||||
await onSubmit(content, activeIds.length > 0 ? activeIds : undefined);
|
||||
editorRef.current?.clearContent();
|
||||
setIsEmpty(true);
|
||||
setPendingAttachments([]);
|
||||
clearDraft(draftKey);
|
||||
} finally {
|
||||
|
||||
@@ -96,10 +96,10 @@ function ReplyInput({
|
||||
.filter((a) => content.includes(a.url))
|
||||
.map((a) => a.id);
|
||||
setSubmitting(true);
|
||||
editorRef.current?.clearContent();
|
||||
setIsEmpty(true);
|
||||
try {
|
||||
await onSubmit(content, activeIds.length > 0 ? activeIds : undefined);
|
||||
editorRef.current?.clearContent();
|
||||
setIsEmpty(true);
|
||||
setPendingAttachments([]);
|
||||
if (draftKey) clearDraft(draftKey);
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
|
||||
import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
@@ -68,6 +68,8 @@ export function useIssueTimeline(issueId: string, userId?: string) {
|
||||
|
||||
const timeline = useMemo<TimelineEntry[]>(() => data ?? [], [data]);
|
||||
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Stable mutation handles. TanStack v5 returns a fresh result wrapper from
|
||||
// useMutation per render, but the inner mutateAsync / mutate functions are
|
||||
// stable. Pull just those so the useCallback identities downstream don't
|
||||
@@ -260,7 +262,8 @@ export function useIssueTimeline(issueId: string, userId?: string) {
|
||||
|
||||
const submitComment = useCallback(
|
||||
async (content: string, attachmentIds?: string[]) => {
|
||||
if (!content.trim() || !userId) return;
|
||||
if (!content.trim() || submitting || !userId) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await createComment({ content, attachmentIds });
|
||||
} catch (err) {
|
||||
@@ -269,9 +272,11 @@ export function useIssueTimeline(issueId: string, userId?: string) {
|
||||
? err.message
|
||||
: t(($) => $.comment.send_failed),
|
||||
);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[userId, createComment, t],
|
||||
[userId, submitting, createComment, t],
|
||||
);
|
||||
|
||||
const submitReply = useCallback(
|
||||
@@ -422,6 +427,7 @@ export function useIssueTimeline(issueId: string, userId?: string) {
|
||||
return {
|
||||
timeline: optimisticTimeline,
|
||||
loading,
|
||||
submitting,
|
||||
submitComment,
|
||||
submitReply,
|
||||
editComment,
|
||||
|
||||
@@ -70,6 +70,9 @@ export function GitHubTab() {
|
||||
const [disconnectTarget, setDisconnectTarget] = useState<string | null>(null);
|
||||
const [disconnecting, setDisconnecting] = useState(false);
|
||||
|
||||
const githubRepoCount =
|
||||
workspace?.repos?.filter((r) => /github\.com/i.test(r.url ?? "")).length ?? 0;
|
||||
|
||||
async function persistSetting(key: SettingsKey, next: boolean) {
|
||||
if (!workspace || savingKey) return;
|
||||
setSavingKey(key);
|
||||
@@ -311,9 +314,14 @@ export function GitHubTab() {
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-sm font-medium">
|
||||
{t(($) => $.github.repositories_shortcut_label)}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t(($) => $.github.repositories_shortcut_label)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground tabular-nums">
|
||||
{githubRepoCount}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
@@ -1002,6 +1002,7 @@ func runIssueCommentList(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("list comments: %w", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Showing %d comments.\n", len(comments))
|
||||
// The server emits the next-page cursor in headers when there is likely
|
||||
// an older page. Surface it on stderr so an operator (and the agent
|
||||
// prompt update that follows this PR) can scroll deeper without having
|
||||
|
||||
@@ -1780,44 +1780,6 @@ func TestRunIssueCommentList_RecentStillLabelsCursorAsThread(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunIssueCommentList_DoesNotPrintShowingPreamble locks in the removal of
|
||||
// the "Showing N comments." stderr preamble. The line was the only
|
||||
// `list --output json` subcommand that emitted a human-readable count, which
|
||||
// polluted stdout/stderr-merged consumers (agent harnesses, CI `2>&1`).
|
||||
// Tracks GitHub issue #3303.
|
||||
func TestRunIssueCommentList_DoesNotPrintShowingPreamble(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/issues/") && !strings.Contains(r.URL.Path, "/comments") {
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "issue-1",
|
||||
"identifier": "MUL-1",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`[{"id":"c1"},{"id":"c2"}]`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
t.Setenv("MULTICA_SERVER_URL", srv.URL)
|
||||
t.Setenv("MULTICA_WORKSPACE_ID", "ws-1")
|
||||
t.Setenv("MULTICA_TOKEN", "test-token")
|
||||
|
||||
stderr := captureStderr(t)
|
||||
defer stderr.restore()
|
||||
|
||||
cmd := newIssueCommentListTestCmd()
|
||||
if err := cmd.Flags().Set("output", "json"); err != nil {
|
||||
t.Fatalf("set output: %v", err)
|
||||
}
|
||||
if err := runIssueCommentList(cmd, []string{"MUL-1"}); err != nil {
|
||||
t.Fatalf("runIssueCommentList: %v", err)
|
||||
}
|
||||
|
||||
if got := stderr.read(); strings.Contains(got, "Showing") {
|
||||
t.Errorf("stderr must not contain a 'Showing ...' preamble, got: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidIssueStatuses(t *testing.T) {
|
||||
expected := map[string]bool{
|
||||
"backlog": true,
|
||||
|
||||
Reference in New Issue
Block a user