refactor: enhance task review notifications and UI components
- Updated task review notification messages for clearer formatting, emphasizing actions taken by reviewers. - Improved styling for task comments and badges to enhance visibility and user interaction. - Added new properties to support better tracking of review states and reviewer information in task details. - Refactored UI components to ensure consistent spacing and layout across task-related dialogs and sections.
This commit is contained in:
parent
ce28f725f9
commit
0b5d4b41f1
9 changed files with 483 additions and 418 deletions
|
|
@ -82,7 +82,7 @@ function requestReview(context, taskId, flags = {}) {
|
|||
to: reviewer,
|
||||
from,
|
||||
text:
|
||||
`Please review task #${task.displayId || task.id}.\n\n` +
|
||||
`**Please review** task #${task.displayId || task.id}\n\n` +
|
||||
wrapAgentBlock(
|
||||
`When approved, use MCP tool review_approve:\n` +
|
||||
`{ teamName: "${context.teamName}", taskId: "${task.id}", notifyOwner: true }\n\n` +
|
||||
|
|
@ -140,8 +140,8 @@ function approveReview(context, taskId, flags = {}) {
|
|||
from,
|
||||
text:
|
||||
note && note !== 'Approved'
|
||||
? `Task #${task.displayId || task.id} approved.\n\n${note}`
|
||||
: `Task #${task.displayId || task.id} approved.`,
|
||||
? `@${from} **approved** task #${task.displayId || task.id}\n\n${note}`
|
||||
: `@${from} **approved** task #${task.displayId || task.id}`,
|
||||
summary: `Approved #${task.displayId || task.id}`,
|
||||
source: 'system_notification',
|
||||
...(leadSessionId ? { leadSessionId } : {}),
|
||||
|
|
@ -192,7 +192,7 @@ function requestChanges(context, taskId, flags = {}) {
|
|||
to: task.owner,
|
||||
from,
|
||||
text:
|
||||
`Task #${task.displayId || task.id} needs fixes.\n\n${comment}\n\n` +
|
||||
`@${from} **requested changes** for task #${task.displayId || task.id}\n\n${comment}\n\n` +
|
||||
'The task has been moved back to pending. When you are ready to resume, review the task context, start it explicitly, implement the fixes, mark it completed, and request review again.',
|
||||
...(Array.isArray(flags.taskRefs) ? { taskRefs: flags.taskRefs } : {}),
|
||||
summary: `Fix request for #${task.displayId || task.id}`,
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ export const TaskCommentsSection = ({
|
|||
<AnimatedHeightReveal key={comment.id} animate={newCommentIds.has(comment.id)}>
|
||||
<div
|
||||
className={[
|
||||
'group px-4 py-2.5',
|
||||
'group min-w-0 overflow-hidden px-4 py-2.5',
|
||||
comment.type === 'review_approved'
|
||||
? 'border-y border-emerald-500/20 bg-emerald-500/5'
|
||||
: comment.type === 'review_request'
|
||||
|
|
@ -299,6 +299,7 @@ export const TaskCommentsSection = ({
|
|||
/>
|
||||
) : (
|
||||
<span
|
||||
className="break-words"
|
||||
onClickCapture={
|
||||
onTaskIdClick
|
||||
? (e) => {
|
||||
|
|
|
|||
|
|
@ -566,6 +566,26 @@ export const TaskDetailDialog = ({
|
|||
<span className="text-xs italic text-[var(--color-text-muted)]">Unassigned</span>
|
||||
)}
|
||||
</div>
|
||||
{currentTask.reviewer ||
|
||||
(currentTask.reviewState && currentTask.reviewState !== 'none') ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Eye size={12} className="text-[var(--color-text-muted)]" />
|
||||
{currentTask.reviewer ? (
|
||||
<MemberBadge
|
||||
name={currentTask.reviewer}
|
||||
color={colorMap.get(currentTask.reviewer)}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
{currentTask.reviewState && currentTask.reviewState !== 'none' ? (
|
||||
<span
|
||||
className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium ${REVIEW_STATE_DISPLAY[currentTask.reviewState].bg} ${REVIEW_STATE_DISPLAY[currentTask.reviewState].text}`}
|
||||
>
|
||||
{REVIEW_STATE_DISPLAY[currentTask.reviewState].label}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{currentTask.createdBy ? (
|
||||
<div className="flex items-center gap-1.5 text-[var(--color-text-muted)]">
|
||||
<PenLine size={12} />
|
||||
|
|
@ -688,6 +708,8 @@ export const TaskDetailDialog = ({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Sections container with uniform spacing */}
|
||||
<div className="space-y-1">
|
||||
{/* Description */}
|
||||
<CollapsibleTeamSection
|
||||
title="Description"
|
||||
|
|
@ -1000,7 +1022,7 @@ export const TaskDetailDialog = ({
|
|||
relatedIds.length > 0 ||
|
||||
relatedByIds.length > 0 ||
|
||||
kanbanTaskState ? (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
{/* Dependencies */}
|
||||
{blockedByIds.length > 0 ? (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
|
|
@ -1027,7 +1049,9 @@ export const TaskDetailDialog = ({
|
|||
}
|
||||
onClick={() => handleDependencyClick(id)}
|
||||
>
|
||||
{depTask ? formatTaskDisplayLabel(depTask) : `#${deriveTaskDisplayId(id)}`}
|
||||
{depTask
|
||||
? formatTaskDisplayLabel(depTask)
|
||||
: `#${deriveTaskDisplayId(id)}`}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1059,7 +1083,9 @@ export const TaskDetailDialog = ({
|
|||
}
|
||||
onClick={() => handleDependencyClick(id)}
|
||||
>
|
||||
{depTask ? formatTaskDisplayLabel(depTask) : `#${deriveTaskDisplayId(id)}`}
|
||||
{depTask
|
||||
? formatTaskDisplayLabel(depTask)
|
||||
: `#${deriveTaskDisplayId(id)}`}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1075,7 +1101,9 @@ export const TaskDetailDialog = ({
|
|||
</span>
|
||||
) : null}
|
||||
{kanbanTaskState.errorDescription ? (
|
||||
<span className="text-xs text-red-400">{kanbanTaskState.errorDescription}</span>
|
||||
<span className="text-xs text-red-400">
|
||||
{kanbanTaskState.errorDescription}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -1128,11 +1156,14 @@ export const TaskDetailDialog = ({
|
|||
hideHeader
|
||||
hideInput
|
||||
onReply={handleReply}
|
||||
onTaskIdClick={onScrollToTask ? (taskId) => handleDependencyClick(taskId) : undefined}
|
||||
onTaskIdClick={
|
||||
onScrollToTask ? (taskId) => handleDependencyClick(taskId) : undefined
|
||||
}
|
||||
containerClassName="-mx-6"
|
||||
unreadCommentIds={unreadSnapshotRef.current}
|
||||
/>
|
||||
</CollapsibleTeamSection>
|
||||
</div>
|
||||
</LightboxLockProvider>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const KanbanColumn = ({
|
|||
</Badge>
|
||||
</div>
|
||||
</header>
|
||||
<div className={cn('flex max-h-[480px] flex-col overflow-auto p-2', bodyClassName)}>
|
||||
<div className={cn('flex max-h-[480px] flex-col gap-1.5 overflow-auto p-2', bodyClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -245,6 +245,12 @@ export const MessageComposer = ({
|
|||
// Track whether we initiated a send — clear draft only on confirmed success
|
||||
const pendingSendRef = useRef(false);
|
||||
|
||||
const handleCycleActionMode = useCallback(() => {
|
||||
const modes: ActionMode[] = canDelegate ? ['do', 'ask', 'delegate'] : ['do', 'ask'];
|
||||
const idx = modes.indexOf(actionMode);
|
||||
setActionMode(modes[(idx + 1) % modes.length]);
|
||||
}, [actionMode, canDelegate, setActionMode]);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
if (!canSend) return;
|
||||
dismissMentionsRef.current?.();
|
||||
|
|
@ -835,6 +841,7 @@ export const MessageComposer = ({
|
|||
projectPath={projectPath}
|
||||
onFileChipInsert={draft.addChip}
|
||||
onModEnter={handleSend}
|
||||
onShiftTab={handleCycleActionMode}
|
||||
dismissMentionsRef={dismissMentionsRef}
|
||||
minRows={2}
|
||||
maxRows={6}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,12 @@ export const MentionSuggestionList = ({
|
|||
/>
|
||||
) : null}
|
||||
{s.subtitle && !isTask ? (
|
||||
<span className="truncate text-[var(--color-text-muted)]">{s.subtitle}</span>
|
||||
<span
|
||||
className="truncate text-[var(--color-text-muted)]"
|
||||
style={isFileOrFolder ? { direction: 'rtl', textAlign: 'left' } : undefined}
|
||||
>
|
||||
{isFileOrFolder ? '\u200E' + s.subtitle : s.subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -287,9 +287,9 @@ function parseSegments(
|
|||
// Default fallback color for mentions without a team color
|
||||
const DEFAULT_MENTION_BG = 'rgba(59, 130, 246, 0.15)';
|
||||
const DEFAULT_MENTION_TEXT = '#60a5fa';
|
||||
const URL_BADGE_BG = 'rgba(37, 99, 235, 0.12)';
|
||||
const URL_BADGE_BORDER = 'rgba(96, 165, 250, 0.22)';
|
||||
const URL_BADGE_TEXT = '#bfdbfe';
|
||||
const URL_BADGE_BG = 'var(--url-badge-bg)';
|
||||
const URL_BADGE_BORDER = 'var(--url-badge-border)';
|
||||
const URL_BADGE_TEXT = 'var(--url-badge-text)';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
|
|
@ -324,6 +324,8 @@ interface MentionableTextareaProps extends Omit<
|
|||
taskSuggestions?: MentionSuggestion[];
|
||||
/** Called when Enter (without Shift) is pressed. */
|
||||
onModEnter?: () => void;
|
||||
/** Called when Shift+Tab is pressed. */
|
||||
onShiftTab?: () => void;
|
||||
/** Ref that receives the dismiss callback to close mention dropdown from outside */
|
||||
dismissMentionsRef?: React.MutableRefObject<(() => void) | null>;
|
||||
}
|
||||
|
|
@ -346,6 +348,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
|||
teamSuggestions = [],
|
||||
taskSuggestions = [],
|
||||
onModEnter,
|
||||
onShiftTab,
|
||||
dismissMentionsRef,
|
||||
style,
|
||||
className,
|
||||
|
|
@ -823,6 +826,12 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
|||
});
|
||||
if (e.defaultPrevented) return;
|
||||
}
|
||||
// Shift+Tab → cycle action mode
|
||||
if (e.key === 'Tab' && e.shiftKey && onShiftTab) {
|
||||
e.preventDefault();
|
||||
onShiftTab();
|
||||
return;
|
||||
}
|
||||
// Enter (without Shift) → submit; Shift+Enter → newline
|
||||
if (e.key === 'Enter' && !e.shiftKey && onModEnter) {
|
||||
e.preventDefault();
|
||||
|
|
@ -834,6 +843,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
|||
},
|
||||
[
|
||||
onModEnter,
|
||||
onShiftTab,
|
||||
handleChipKeyDown,
|
||||
mentionHandleKeyDown,
|
||||
isOpen,
|
||||
|
|
@ -1030,14 +1040,14 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
|||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex max-w-full items-center rounded-full px-1.5 py-0 align-baseline text-[0.92em] font-medium"
|
||||
style={{
|
||||
backgroundColor: URL_BADGE_BG,
|
||||
color: URL_BADGE_TEXT,
|
||||
borderRadius: '4px',
|
||||
boxShadow: `inset 0 0 0 1px ${URL_BADGE_BORDER}`,
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{seg.value}</span>
|
||||
{seg.value}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,12 @@ const badgeVariants = cva(
|
|||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
|
||||
|
||||
const Badge = ({ className, variant, ...props }: BadgeProps): React.JSX.Element => {
|
||||
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
};
|
||||
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
({ className, variant, ...props }, ref) => {
|
||||
return <span ref={ref} className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
);
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components -- Standard shadcn export pattern
|
||||
export { Badge, badgeVariants };
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@
|
|||
/* Inline code */
|
||||
--inline-code-bg: rgba(148, 163, 184, 0.08);
|
||||
--inline-code-text: #e2e8f0;
|
||||
/* URL badge (textarea overlay) */
|
||||
--url-badge-bg: rgba(37, 99, 235, 0.12);
|
||||
--url-badge-border: rgba(96, 165, 250, 0.22);
|
||||
--url-badge-text: #bfdbfe;
|
||||
/* Diff viewer */
|
||||
--diff-added-bg: rgba(34, 197, 94, 0.15);
|
||||
--diff-added-text: #4ade80;
|
||||
|
|
@ -440,6 +444,10 @@
|
|||
/* Inline code - Warm neutral */
|
||||
--inline-code-bg: rgba(0, 0, 0, 0.05);
|
||||
--inline-code-text: #3a3935;
|
||||
/* URL badge (textarea overlay) - Light mode */
|
||||
--url-badge-bg: rgba(37, 99, 235, 0.1);
|
||||
--url-badge-border: rgba(37, 99, 235, 0.25);
|
||||
--url-badge-text: #1d4ed8;
|
||||
/* Diff viewer - Light mode */
|
||||
--diff-added-bg: rgba(34, 197, 94, 0.18);
|
||||
--diff-added-text: #14532d;
|
||||
|
|
|
|||
Loading…
Reference in a new issue