- Introduced a new structured task reference format `{ taskId, displayId, teamName }` for consistent task mention persistence across UI and storage.
- Enhanced message handling in various components to support the new task reference structure, including normalization and validation.
- Updated task-related functions to accommodate optional task reference fields, improving task management and messaging capabilities.
- Improved rendering and navigation of task references in the UI, ensuring stable links across messages and comments.
- Refactored task reference utilities for better integration and usability within the application.
130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { useMemo } from 'react';
|
|
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@renderer/components/ui/dialog';
|
|
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
|
|
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
|
import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
|
|
import { useStore } from '@renderer/store';
|
|
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
|
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
|
import {
|
|
extractTaskRefsFromText,
|
|
stripEncodedTaskReferenceMetadata,
|
|
} from '@renderer/utils/taskReferenceUtils';
|
|
import { MAX_TEXT_LENGTH } from '@shared/constants';
|
|
import { deriveTaskDisplayId } from '@shared/utils/taskIdentity';
|
|
import { Send } from 'lucide-react';
|
|
|
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
|
import type { ResolvedTeamMember, TaskRef } from '@shared/types';
|
|
|
|
interface ReviewDialogProps {
|
|
open: boolean;
|
|
teamName: string;
|
|
taskId: string | null;
|
|
members: ResolvedTeamMember[];
|
|
onCancel: () => void;
|
|
onSubmit: (comment?: string, taskRefs?: TaskRef[]) => void;
|
|
}
|
|
|
|
export const ReviewDialog = ({
|
|
open,
|
|
teamName,
|
|
taskId,
|
|
members,
|
|
onCancel,
|
|
onSubmit,
|
|
}: ReviewDialogProps): React.JSX.Element => {
|
|
const projectPath = useStore((s) => s.selectedTeamData?.config.projectPath ?? null);
|
|
const { suggestions: taskSuggestions } = useTaskSuggestions(teamName);
|
|
const draft = useDraftPersistence({
|
|
key: `requestChanges:${teamName}:${taskId ?? ''}`,
|
|
enabled: Boolean(teamName && taskId),
|
|
});
|
|
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
|
|
|
const mentionSuggestions = useMemo<MentionSuggestion[]>(
|
|
() =>
|
|
members.map((m) => ({
|
|
id: m.name,
|
|
name: m.name,
|
|
subtitle: formatAgentRole(m.role) ?? formatAgentRole(m.agentType) ?? undefined,
|
|
color: colorMap.get(m.name),
|
|
})),
|
|
[members, colorMap]
|
|
);
|
|
|
|
const trimmed = stripEncodedTaskReferenceMetadata(draft.value).trim();
|
|
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
|
|
|
const handleSubmit = (): void => {
|
|
const comment = stripEncodedTaskReferenceMetadata(trimmed) || undefined;
|
|
const taskRefs = trimmed ? extractTaskRefsFromText(draft.value, taskSuggestions) : [];
|
|
draft.clearDraft();
|
|
onSubmit(comment, taskRefs);
|
|
};
|
|
|
|
return (
|
|
<Dialog
|
|
open={open && taskId !== null}
|
|
onOpenChange={(nextOpen) => {
|
|
if (!nextOpen) {
|
|
onCancel();
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent className="sm:max-w-4xl">
|
|
<DialogHeader>
|
|
<DialogTitle>Request Changes</DialogTitle>
|
|
<DialogDescription>Task #{taskId ? deriveTaskDisplayId(taskId) : ''}</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="grid gap-2 py-2">
|
|
<MentionableTextarea
|
|
id="review-comment"
|
|
value={draft.value}
|
|
onValueChange={draft.setValue}
|
|
placeholder="Describe what needs to change... (Enter to submit)"
|
|
suggestions={mentionSuggestions}
|
|
taskSuggestions={taskSuggestions}
|
|
projectPath={projectPath}
|
|
onModEnter={handleSubmit}
|
|
minRows={4}
|
|
maxRows={12}
|
|
maxLength={MAX_TEXT_LENGTH}
|
|
cornerAction={
|
|
<button
|
|
type="button"
|
|
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-red-600 px-3 py-1.5 text-[11px] font-medium text-white shadow-sm transition-colors hover:bg-red-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
onClick={handleSubmit}
|
|
>
|
|
<Send size={12} />
|
|
Submit
|
|
</button>
|
|
}
|
|
footerRight={
|
|
<div className="flex items-center gap-2">
|
|
{remaining < 200 ? (
|
|
<span
|
|
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
|
|
>
|
|
{remaining} chars left
|
|
</span>
|
|
) : null}
|
|
{draft.isSaved ? (
|
|
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
|
|
) : null}
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|