mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-23 18:17:44 +02:00
* fix(issues): stop issue-trigger preview flicker The pre-trigger preview re-rendered/refetched on every workspace task event: WS task lifecycle invalidated issueTriggerPreviewAll (staleTime 0), forcing a background refetch whose isFetching was surfaced as isLoading, collapsing and reopening CreateRunHint's reveal band. The assign source (create / assignee change) cancels existing tasks before enqueuing, so its verdict can't shift from a task event at all; the status source's pending dedup could, but the preview is advisory and the write path re-evaluates authoritatively, so a rare stale label is harmless. Drop the WS invalidation so the preview refetches only on input (signature) change. Keep the comment-trigger invalidation — its verdict genuinely changes mid-compose and its chips drive an immediate, unconfirmed send. Align the hook's data handling with the comment-trigger preview: keepPreviousData so an input switch swaps in place instead of collapsing, and treat only the first load (no prior data) as loading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(issues): skip run-confirm modal for backlog assign Assigning a Backlog issue to an agent/squad never starts a run (the parking lot — server/internal/service/issue_trigger.go), so the pre-trigger confirm modal only rendered an empty "won't start" box with a single Apply button. Apply directly instead: the single path checks issue.status, the batch path skips only when every selected issue is Backlog (mixed selections still confirm — the non-backlog ones trigger). Mirrors the existing backlog short-circuit in handleBatchStatus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(modals): run-confirm loading state + submit spinner The dialog grew in height after open: it rendered the short "won't start" variant while POST /api/issues/preview-trigger was in flight, then the note box appeared when the predicate landed. Keep the note box mounted (disabled) during loading so assign mode opens at its resolved height, and show a Spinner + 'checking' headline while loading. Submit had no feedback — buttons only disabled, which read as frozen for note assigns (the request starts an agent server-side). Track which footer action is in flight and show a Spinner on the clicked button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(issues): show handoff note in execution-log trigger text An assignment-triggered run that carried a handoff note showed the generic "Initial run" label. Surface the note inline (truncated, like comment triggers show their text) so the row reads as the handoff. taskToResponse now populates handoff_note for all callers (dropping the now-redundant explicit set in ClaimTaskByRuntime); the field is added to the AgentTask type + zod schema (optional, additive — old clients ignore it via the loose schema, new clients fall back to "Initial run"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96 lines
3.8 KiB
TypeScript
96 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
import { api } from "@multica/core/api";
|
|
import { issueKeys } from "@multica/core/issues/queries";
|
|
import type { IssueAssigneeType, IssueStatus, IssueTriggerPreviewItem } from "@multica/core/types";
|
|
|
|
export interface UseIssueTriggerPreviewParams {
|
|
/** Existing issues to evaluate (single assign/status or batch). */
|
|
issueIds?: string[];
|
|
/** Preview a not-yet-persisted issue from assignee/status (create modal). */
|
|
isCreate?: boolean;
|
|
assigneeType?: IssueAssigneeType | null;
|
|
assigneeId?: string | null;
|
|
status?: IssueStatus;
|
|
/** Caller gate — e.g. only fetch while a picker/modal is open. */
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface UseIssueTriggerPreviewResult {
|
|
triggers: IssueTriggerPreviewItem[];
|
|
totalCount: number;
|
|
isLoading: boolean;
|
|
/** True when every trigger's target runtime can render a handoff note, so
|
|
* the note box is safe to enable. False if any started run would drop it. */
|
|
handoffSupported: boolean;
|
|
}
|
|
|
|
const EMPTY: IssueTriggerPreviewItem[] = [];
|
|
|
|
function previewSignature(params: UseIssueTriggerPreviewParams): string {
|
|
return JSON.stringify({
|
|
ids: [...(params.issueIds ?? [])].sort(),
|
|
create: params.isCreate ?? false,
|
|
at: params.assigneeType ?? null,
|
|
aid: params.assigneeId ?? null,
|
|
status: params.status ?? null,
|
|
});
|
|
}
|
|
|
|
/** Reads the unified backend predicate via POST /api/issues/preview-trigger so
|
|
* the four entry points never re-implement "will this start a run" (MUL-3375).
|
|
*
|
|
* The verdict changes only with the inputs (assignee / status), so the query
|
|
* refetches solely on signature change — it is deliberately NOT invalidated by
|
|
* WS task events. The assign source (create / assignee change) cancels existing
|
|
* tasks before enqueuing, so its verdict can't shift from a task event at all;
|
|
* the status source's pending dedup could, but the preview is advisory and the
|
|
* write path re-evaluates authoritatively, so a rare stale status label is
|
|
* harmless — far better than refetching every mounted preview on every
|
|
* workspace task event (the source of the visible flicker, MUL-3375).
|
|
*
|
|
* Mirrors the comment-trigger preview's data handling: keepPreviousData so an
|
|
* input switch swaps the answer in place instead of collapsing, and only the
|
|
* very first load (no prior data) counts as loading. */
|
|
export function useIssueTriggerPreview(
|
|
params: UseIssueTriggerPreviewParams,
|
|
): UseIssueTriggerPreviewResult {
|
|
const hasTarget =
|
|
(!!params.assigneeType && !!params.assigneeId) ||
|
|
!!params.status ||
|
|
(params.isCreate ?? false);
|
|
const enabled = (params.enabled ?? true) && hasTarget;
|
|
|
|
const signature = useMemo(() => previewSignature(params), [params]);
|
|
|
|
const previewQuery = useQuery({
|
|
queryKey: issueKeys.issueTriggerPreview(signature),
|
|
queryFn: () =>
|
|
api.previewIssueTrigger({
|
|
issueIds: params.issueIds,
|
|
isCreate: params.isCreate,
|
|
assigneeType: params.assigneeType,
|
|
assigneeId: params.assigneeId,
|
|
status: params.status,
|
|
}),
|
|
enabled,
|
|
retry: false,
|
|
staleTime: 0,
|
|
// Keep the prior verdict visible while a new signature (assignee/status
|
|
// switch) refetches, so the hint swaps in place rather than collapsing.
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
|
|
const triggers = previewQuery.data?.triggers ?? EMPTY;
|
|
return {
|
|
triggers,
|
|
totalCount: previewQuery.data?.total_count ?? 0,
|
|
// Only the first load (no prior data) is "loading"; a background/placeholder
|
|
// refetch is not, so reveal animations gated on this never collapse mid-fetch.
|
|
isLoading: enabled && previewQuery.isLoading,
|
|
handoffSupported: triggers.length > 0 && triggers.every((t) => t.handoff_supported === true),
|
|
};
|
|
}
|