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,
|
to: reviewer,
|
||||||
from,
|
from,
|
||||||
text:
|
text:
|
||||||
`Please review task #${task.displayId || task.id}.\n\n` +
|
`**Please review** task #${task.displayId || task.id}\n\n` +
|
||||||
wrapAgentBlock(
|
wrapAgentBlock(
|
||||||
`When approved, use MCP tool review_approve:\n` +
|
`When approved, use MCP tool review_approve:\n` +
|
||||||
`{ teamName: "${context.teamName}", taskId: "${task.id}", notifyOwner: true }\n\n` +
|
`{ teamName: "${context.teamName}", taskId: "${task.id}", notifyOwner: true }\n\n` +
|
||||||
|
|
@ -140,8 +140,8 @@ function approveReview(context, taskId, flags = {}) {
|
||||||
from,
|
from,
|
||||||
text:
|
text:
|
||||||
note && note !== 'Approved'
|
note && note !== 'Approved'
|
||||||
? `Task #${task.displayId || task.id} approved.\n\n${note}`
|
? `@${from} **approved** task #${task.displayId || task.id}\n\n${note}`
|
||||||
: `Task #${task.displayId || task.id} approved.`,
|
: `@${from} **approved** task #${task.displayId || task.id}`,
|
||||||
summary: `Approved #${task.displayId || task.id}`,
|
summary: `Approved #${task.displayId || task.id}`,
|
||||||
source: 'system_notification',
|
source: 'system_notification',
|
||||||
...(leadSessionId ? { leadSessionId } : {}),
|
...(leadSessionId ? { leadSessionId } : {}),
|
||||||
|
|
@ -192,7 +192,7 @@ function requestChanges(context, taskId, flags = {}) {
|
||||||
to: task.owner,
|
to: task.owner,
|
||||||
from,
|
from,
|
||||||
text:
|
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.',
|
'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 } : {}),
|
...(Array.isArray(flags.taskRefs) ? { taskRefs: flags.taskRefs } : {}),
|
||||||
summary: `Fix request for #${task.displayId || task.id}`,
|
summary: `Fix request for #${task.displayId || task.id}`,
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export const TaskCommentsSection = ({
|
||||||
<AnimatedHeightReveal key={comment.id} animate={newCommentIds.has(comment.id)}>
|
<AnimatedHeightReveal key={comment.id} animate={newCommentIds.has(comment.id)}>
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'group px-4 py-2.5',
|
'group min-w-0 overflow-hidden px-4 py-2.5',
|
||||||
comment.type === 'review_approved'
|
comment.type === 'review_approved'
|
||||||
? 'border-y border-emerald-500/20 bg-emerald-500/5'
|
? 'border-y border-emerald-500/20 bg-emerald-500/5'
|
||||||
: comment.type === 'review_request'
|
: comment.type === 'review_request'
|
||||||
|
|
@ -299,6 +299,7 @@ export const TaskCommentsSection = ({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
|
className="break-words"
|
||||||
onClickCapture={
|
onClickCapture={
|
||||||
onTaskIdClick
|
onTaskIdClick
|
||||||
? (e) => {
|
? (e) => {
|
||||||
|
|
|
||||||
|
|
@ -566,6 +566,26 @@ export const TaskDetailDialog = ({
|
||||||
<span className="text-xs italic text-[var(--color-text-muted)]">Unassigned</span>
|
<span className="text-xs italic text-[var(--color-text-muted)]">Unassigned</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 ? (
|
{currentTask.createdBy ? (
|
||||||
<div className="flex items-center gap-1.5 text-[var(--color-text-muted)]">
|
<div className="flex items-center gap-1.5 text-[var(--color-text-muted)]">
|
||||||
<PenLine size={12} />
|
<PenLine size={12} />
|
||||||
|
|
@ -688,451 +708,462 @@ export const TaskDetailDialog = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Description */}
|
{/* Sections container with uniform spacing */}
|
||||||
<CollapsibleTeamSection
|
<div className="space-y-1">
|
||||||
title="Description"
|
{/* Description */}
|
||||||
icon={<AlignLeft size={14} />}
|
<CollapsibleTeamSection
|
||||||
contentClassName="pl-2.5"
|
title="Description"
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
icon={<AlignLeft size={14} />}
|
||||||
headerContentClassName="pl-6"
|
contentClassName="pl-2.5"
|
||||||
defaultOpen
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
>
|
headerContentClassName="pl-6"
|
||||||
{editingDescription ? (
|
defaultOpen
|
||||||
<div className="space-y-2">
|
>
|
||||||
<div className="flex items-center gap-2">
|
{editingDescription ? (
|
||||||
<button
|
<div className="space-y-2">
|
||||||
type="button"
|
<div className="flex items-center gap-2">
|
||||||
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
|
|
||||||
!descriptionPreview
|
|
||||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
|
||||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => setDescriptionPreview(false)}
|
|
||||||
>
|
|
||||||
<Pencil size={12} />
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
|
|
||||||
descriptionPreview
|
|
||||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
|
||||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
|
||||||
}`}
|
|
||||||
onClick={() => setDescriptionPreview(true)}
|
|
||||||
>
|
|
||||||
<Eye size={12} />
|
|
||||||
Preview
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{descriptionPreview ? (
|
|
||||||
<div className="max-h-[200px] overflow-y-auto rounded border border-[var(--color-border)] p-2">
|
|
||||||
{descriptionDraft.trim() ? (
|
|
||||||
<MarkdownViewer content={descriptionDraft} maxHeight="max-h-[180px]" />
|
|
||||||
) : (
|
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">Nothing to preview</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Textarea
|
|
||||||
autoFocus
|
|
||||||
value={descriptionDraft}
|
|
||||||
onChange={(e) => setDescriptionDraft(e.target.value)}
|
|
||||||
disabled={savingDescription}
|
|
||||||
rows={6}
|
|
||||||
className="text-xs"
|
|
||||||
placeholder="Task description (supports markdown)"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs"
|
|
||||||
disabled={savingDescription}
|
|
||||||
onClick={() => void saveDescription()}
|
|
||||||
>
|
|
||||||
{savingDescription ? (
|
|
||||||
<Loader2 size={12} className="mr-1 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Check size={12} className="mr-1" />
|
|
||||||
)}
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 text-xs"
|
|
||||||
disabled={savingDescription}
|
|
||||||
onClick={() => setEditingDescription(false)}
|
|
||||||
>
|
|
||||||
<X size={12} className="mr-1" />
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : currentTask.description ? (
|
|
||||||
<div className="group relative">
|
|
||||||
<ExpandableContent collapsedHeight={200}>
|
|
||||||
<MarkdownViewer content={currentTask.description} maxHeight="max-h-none" bare />
|
|
||||||
</ExpandableContent>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-0 top-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)] group-hover:opacity-100"
|
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
|
||||||
onClick={startEditDescription}
|
!descriptionPreview
|
||||||
|
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
||||||
|
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => setDescriptionPreview(false)}
|
||||||
>
|
>
|
||||||
<Pencil size={12} />
|
<Pencil size={12} />
|
||||||
|
Edit
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
<button
|
||||||
<TooltipContent side="top">Edit description</TooltipContent>
|
type="button"
|
||||||
</Tooltip>
|
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
|
||||||
</div>
|
descriptionPreview
|
||||||
) : (
|
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
||||||
<button
|
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
||||||
type="button"
|
}`}
|
||||||
className="text-xs text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
onClick={() => setDescriptionPreview(true)}
|
||||||
onClick={startEditDescription}
|
>
|
||||||
>
|
<Eye size={12} />
|
||||||
Click to add description...
|
Preview
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</CollapsibleTeamSection>
|
{descriptionPreview ? (
|
||||||
|
<div className="max-h-[200px] overflow-y-auto rounded border border-[var(--color-border)] p-2">
|
||||||
{/* Attachments */}
|
{descriptionDraft.trim() ? (
|
||||||
<CollapsibleTeamSection
|
<MarkdownViewer content={descriptionDraft} maxHeight="max-h-[180px]" />
|
||||||
title="Attachments"
|
) : (
|
||||||
icon={<ImageIcon size={14} />}
|
<p className="text-xs text-[var(--color-text-muted)]">Nothing to preview</p>
|
||||||
badge={
|
)}
|
||||||
(currentTask.attachments?.length ?? 0) + commentImageAttachments.length > 0
|
</div>
|
||||||
? (currentTask.attachments?.length ?? 0) + commentImageAttachments.length
|
) : (
|
||||||
: undefined
|
<Textarea
|
||||||
}
|
autoFocus
|
||||||
contentClassName="pl-2.5"
|
value={descriptionDraft}
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
onChange={(e) => setDescriptionDraft(e.target.value)}
|
||||||
headerContentClassName="pl-6"
|
disabled={savingDescription}
|
||||||
defaultOpen={
|
rows={6}
|
||||||
(currentTask.attachments?.length ?? 0) > 0 || commentImageAttachments.length > 0
|
className="text-xs"
|
||||||
}
|
placeholder="Task description (supports markdown)"
|
||||||
>
|
/>
|
||||||
<TaskAttachments
|
)}
|
||||||
teamName={teamName}
|
<div className="flex items-center gap-2">
|
||||||
taskId={currentTask.id}
|
<Button
|
||||||
attachments={currentTask.attachments ?? []}
|
size="sm"
|
||||||
/>
|
className="h-7 text-xs"
|
||||||
{commentImageAttachments.length > 0 ? (
|
disabled={savingDescription}
|
||||||
<CommentImagesGrid
|
onClick={() => void saveDescription()}
|
||||||
items={commentImageAttachments}
|
>
|
||||||
teamName={teamName}
|
{savingDescription ? (
|
||||||
taskId={currentTask.id}
|
<Loader2 size={12} className="mr-1 animate-spin" />
|
||||||
/>
|
) : (
|
||||||
) : null}
|
<Check size={12} className="mr-1" />
|
||||||
</CollapsibleTeamSection>
|
)}
|
||||||
|
Save
|
||||||
{/* Changes */}
|
</Button>
|
||||||
{variant === 'team' && canShowTaskChanges && onViewChanges ? (
|
<Button
|
||||||
<CollapsibleTeamSection
|
variant="ghost"
|
||||||
key={`task-changes:${currentTask.id}`}
|
size="sm"
|
||||||
title="Changes"
|
className="h-7 text-xs"
|
||||||
icon={<FileDiff size={14} />}
|
disabled={savingDescription}
|
||||||
badge={taskChangesFiles ? taskChangesFiles.length : undefined}
|
onClick={() => setEditingDescription(false)}
|
||||||
headerExtra={
|
>
|
||||||
changesSectionOpen ? (
|
<X size={12} className="mr-1" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : currentTask.description ? (
|
||||||
|
<div className="group relative">
|
||||||
|
<ExpandableContent collapsedHeight={200}>
|
||||||
|
<MarkdownViewer content={currentTask.description} maxHeight="max-h-none" bare />
|
||||||
|
</ExpandableContent>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="pointer-events-auto rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-section-hover)] hover:text-[var(--color-text)] disabled:opacity-50"
|
className="absolute right-0 top-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)] group-hover:opacity-100"
|
||||||
onClick={(e) => {
|
onClick={startEditDescription}
|
||||||
e.stopPropagation();
|
|
||||||
handleRefreshChanges();
|
|
||||||
}}
|
|
||||||
disabled={taskChangesLoading}
|
|
||||||
aria-label="Refresh changes"
|
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<Pencil size={12} />
|
||||||
size={12}
|
|
||||||
className={taskChangesLoading ? 'animate-spin' : undefined}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">Refresh</TooltipContent>
|
<TooltipContent side="top">Edit description</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={startEditDescription}
|
||||||
|
>
|
||||||
|
Click to add description...
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</CollapsibleTeamSection>
|
||||||
|
|
||||||
|
{/* Attachments */}
|
||||||
|
<CollapsibleTeamSection
|
||||||
|
title="Attachments"
|
||||||
|
icon={<ImageIcon size={14} />}
|
||||||
|
badge={
|
||||||
|
(currentTask.attachments?.length ?? 0) + commentImageAttachments.length > 0
|
||||||
|
? (currentTask.attachments?.length ?? 0) + commentImageAttachments.length
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
contentClassName="pl-2.5"
|
contentClassName="pl-2.5"
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
headerContentClassName="pl-6"
|
headerContentClassName="pl-6"
|
||||||
defaultOpen={false}
|
defaultOpen={
|
||||||
onOpenChange={handleChangesSectionOpenChange}
|
(currentTask.attachments?.length ?? 0) > 0 || commentImageAttachments.length > 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{taskChangesLoading ? (
|
<TaskAttachments
|
||||||
<div className="flex items-center gap-2 py-2 text-xs text-[var(--color-text-muted)]">
|
teamName={teamName}
|
||||||
<Loader2 size={14} className="animate-spin" />
|
taskId={currentTask.id}
|
||||||
Loading changes...
|
attachments={currentTask.attachments ?? []}
|
||||||
</div>
|
/>
|
||||||
) : taskChangesError ? (
|
{commentImageAttachments.length > 0 ? (
|
||||||
<p className="text-xs text-red-400">{taskChangesError}</p>
|
<CommentImagesGrid
|
||||||
) : taskChangesFiles && taskChangesFiles.length > 0 ? (
|
items={commentImageAttachments}
|
||||||
<div className="max-h-[200px] space-y-0.5 overflow-y-auto">
|
teamName={teamName}
|
||||||
{taskChangesFiles.map((file) => (
|
taskId={currentTask.id}
|
||||||
<div
|
/>
|
||||||
key={file.filePath}
|
) : null}
|
||||||
className="group flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors hover:bg-[var(--color-surface-raised)]"
|
</CollapsibleTeamSection>
|
||||||
>
|
|
||||||
<FileIcon
|
{/* Changes */}
|
||||||
fileName={file.relativePath.split('/').pop() ?? file.relativePath}
|
{variant === 'team' && canShowTaskChanges && onViewChanges ? (
|
||||||
className="size-3.5"
|
<CollapsibleTeamSection
|
||||||
/>
|
key={`task-changes:${currentTask.id}`}
|
||||||
<button
|
title="Changes"
|
||||||
type="button"
|
icon={<FileDiff size={14} />}
|
||||||
className="min-w-0 flex-1 truncate text-left font-mono text-[var(--color-text-secondary)] transition-colors hover:text-[var(--color-text)]"
|
badge={taskChangesFiles ? taskChangesFiles.length : undefined}
|
||||||
onClick={() => {
|
headerExtra={
|
||||||
handleClose();
|
changesSectionOpen ? (
|
||||||
onViewChanges(currentTask.id, file.filePath);
|
<Tooltip>
|
||||||
}}
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="pointer-events-auto rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-section-hover)] hover:text-[var(--color-text)] disabled:opacity-50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRefreshChanges();
|
||||||
|
}}
|
||||||
|
disabled={taskChangesLoading}
|
||||||
|
aria-label="Refresh changes"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
size={12}
|
||||||
|
className={taskChangesLoading ? 'animate-spin' : undefined}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Refresh</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
contentClassName="pl-2.5"
|
||||||
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
|
headerContentClassName="pl-6"
|
||||||
|
defaultOpen={false}
|
||||||
|
onOpenChange={handleChangesSectionOpenChange}
|
||||||
|
>
|
||||||
|
{taskChangesLoading ? (
|
||||||
|
<div className="flex items-center gap-2 py-2 text-xs text-[var(--color-text-muted)]">
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
Loading changes...
|
||||||
|
</div>
|
||||||
|
) : taskChangesError ? (
|
||||||
|
<p className="text-xs text-red-400">{taskChangesError}</p>
|
||||||
|
) : taskChangesFiles && taskChangesFiles.length > 0 ? (
|
||||||
|
<div className="max-h-[200px] space-y-0.5 overflow-y-auto">
|
||||||
|
{taskChangesFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.filePath}
|
||||||
|
className="group flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs transition-colors hover:bg-[var(--color-surface-raised)]"
|
||||||
>
|
>
|
||||||
{file.relativePath}
|
<FileIcon
|
||||||
</button>
|
fileName={file.relativePath.split('/').pop() ?? file.relativePath}
|
||||||
<span className="flex shrink-0 items-center gap-1.5">
|
className="size-3.5"
|
||||||
{file.linesAdded > 0 ? (
|
/>
|
||||||
<span className="text-emerald-400">+{file.linesAdded}</span>
|
<button
|
||||||
) : null}
|
type="button"
|
||||||
{file.linesRemoved > 0 ? (
|
className="min-w-0 flex-1 truncate text-left font-mono text-[var(--color-text-secondary)] transition-colors hover:text-[var(--color-text)]"
|
||||||
<span className="text-red-400">-{file.linesRemoved}</span>
|
onClick={() => {
|
||||||
) : null}
|
handleClose();
|
||||||
</span>
|
onViewChanges(currentTask.id, file.filePath);
|
||||||
<span className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
}}
|
||||||
<Tooltip>
|
>
|
||||||
<TooltipTrigger asChild>
|
{file.relativePath}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<span className="flex shrink-0 items-center gap-1.5">
|
||||||
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-border-emphasis)] hover:text-[var(--color-text)]"
|
{file.linesAdded > 0 ? (
|
||||||
onClick={() => {
|
<span className="text-emerald-400">+{file.linesAdded}</span>
|
||||||
handleClose();
|
) : null}
|
||||||
onViewChanges(currentTask.id, file.filePath);
|
{file.linesRemoved > 0 ? (
|
||||||
}}
|
<span className="text-red-400">-{file.linesRemoved}</span>
|
||||||
>
|
) : null}
|
||||||
<GitCompareArrows size={13} />
|
</span>
|
||||||
</button>
|
<span className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top">Review diff</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
{onOpenInEditor ? (
|
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-border-emphasis)] hover:text-[var(--color-text)]"
|
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-border-emphasis)] hover:text-[var(--color-text)]"
|
||||||
onClick={() => onOpenInEditor(file.filePath)}
|
onClick={() => {
|
||||||
|
handleClose();
|
||||||
|
onViewChanges(currentTask.id, file.filePath);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<SquarePen size={13} />
|
<GitCompareArrows size={13} />
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top">Open in editor</TooltipContent>
|
<TooltipContent side="top">Review diff</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
{onOpenInEditor ? (
|
||||||
</span>
|
<Tooltip>
|
||||||
</div>
|
<TooltipTrigger asChild>
|
||||||
))}
|
<button
|
||||||
</div>
|
type="button"
|
||||||
) : changesSectionOpen ? (
|
className="rounded p-1 text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-border-emphasis)] hover:text-[var(--color-text)]"
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">No file changes detected</p>
|
onClick={() => onOpenInEditor(file.filePath)}
|
||||||
) : null}
|
>
|
||||||
</CollapsibleTeamSection>
|
<SquarePen size={13} />
|
||||||
) : null}
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Open in editor</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : changesSectionOpen ? (
|
||||||
|
<p className="text-xs text-[var(--color-text-muted)]">No file changes detected</p>
|
||||||
|
) : null}
|
||||||
|
</CollapsibleTeamSection>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Execution Logs — sessions that reference this task */}
|
{/* Execution Logs — sessions that reference this task */}
|
||||||
{variant === 'team' ? (
|
{variant === 'team' ? (
|
||||||
|
<CollapsibleTeamSection
|
||||||
|
key={`task-logs:${currentTask.id}`}
|
||||||
|
title="Execution Logs"
|
||||||
|
icon={<ScrollText size={14} />}
|
||||||
|
headerExtra={
|
||||||
|
logsRefreshing || executionPreviewOnline ? (
|
||||||
|
<span className="flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
{executionPreviewOnline ? (
|
||||||
|
<span
|
||||||
|
className="pointer-events-none relative inline-flex size-2 shrink-0"
|
||||||
|
title="Online"
|
||||||
|
>
|
||||||
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-50" />
|
||||||
|
<span className="relative inline-flex size-2 rounded-full bg-emerald-400" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{logsRefreshing ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Loader2 size={10} className="animate-spin" />
|
||||||
|
Updating...
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
contentClassName="pl-2.5"
|
||||||
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
|
headerContentClassName="pl-6"
|
||||||
|
defaultOpen={false}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<MemberLogsTab
|
||||||
|
teamName={teamName}
|
||||||
|
taskId={currentTask.id}
|
||||||
|
taskOwner={currentTask.owner}
|
||||||
|
taskStatus={currentTask.status}
|
||||||
|
taskWorkIntervals={currentTask.workIntervals}
|
||||||
|
taskSince={taskSince}
|
||||||
|
onRefreshingChange={setLogsRefreshing}
|
||||||
|
// Only show a "latest messages" preview when this task is owned by a subagent.
|
||||||
|
// For lead-owned tasks, the lead session is a mixed stream (lead + multiple agents),
|
||||||
|
// so filtering to "just the member messages" is unreliable and easy to mislead.
|
||||||
|
showSubagentPreview={Boolean(currentTask.owner) && !isLeadOwnedTask}
|
||||||
|
// Temporary debug option: for lead-owned tasks, show quick preview from lead session.
|
||||||
|
showLeadPreview={allowLeadExecutionPreview && isLeadOwnedTask}
|
||||||
|
onPreviewOnlineChange={setExecutionPreviewOnline}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTeamSection>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{blockedByIds.length > 0 ||
|
||||||
|
blocksIds.length > 0 ||
|
||||||
|
relatedIds.length > 0 ||
|
||||||
|
relatedByIds.length > 0 ||
|
||||||
|
kanbanTaskState ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{/* Dependencies */}
|
||||||
|
{blockedByIds.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-xs text-yellow-300">
|
||||||
|
<ArrowLeftFromLine size={12} />
|
||||||
|
Blocked by
|
||||||
|
</span>
|
||||||
|
{blockedByIds.map((id) => {
|
||||||
|
const depTask = taskMap.get(id);
|
||||||
|
const isCompleted = depTask?.status === 'completed';
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
||||||
|
isCompleted
|
||||||
|
? 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25'
|
||||||
|
: 'bg-yellow-500/15 text-yellow-300 hover:bg-yellow-500/25'
|
||||||
|
} cursor-pointer`}
|
||||||
|
title={
|
||||||
|
depTask
|
||||||
|
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
||||||
|
: `#${deriveTaskDisplayId(id)}`
|
||||||
|
}
|
||||||
|
onClick={() => handleDependencyClick(id)}
|
||||||
|
>
|
||||||
|
{depTask
|
||||||
|
? formatTaskDisplayLabel(depTask)
|
||||||
|
: `#${deriveTaskDisplayId(id)}`}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{blocksIds.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="inline-flex items-center gap-0.5 text-xs text-blue-400">
|
||||||
|
<ArrowRightFromLine size={12} />
|
||||||
|
Blocks
|
||||||
|
</span>
|
||||||
|
{blocksIds.map((id) => {
|
||||||
|
const depTask = taskMap.get(id);
|
||||||
|
const isCompleted = depTask?.status === 'completed';
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
type="button"
|
||||||
|
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
||||||
|
isCompleted
|
||||||
|
? 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25'
|
||||||
|
: 'bg-blue-500/15 text-blue-400 hover:bg-blue-500/25'
|
||||||
|
} cursor-pointer`}
|
||||||
|
title={
|
||||||
|
depTask
|
||||||
|
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
||||||
|
: `#${deriveTaskDisplayId(id)}`
|
||||||
|
}
|
||||||
|
onClick={() => handleDependencyClick(id)}
|
||||||
|
>
|
||||||
|
{depTask
|
||||||
|
? formatTaskDisplayLabel(depTask)
|
||||||
|
: `#${deriveTaskDisplayId(id)}`}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Review info */}
|
||||||
|
{kanbanTaskState ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{kanbanTaskState.reviewer ? (
|
||||||
|
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||||
|
Reviewer: {kanbanTaskState.reviewer}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{kanbanTaskState.errorDescription ? (
|
||||||
|
<span className="text-xs text-red-400">
|
||||||
|
{kanbanTaskState.errorDescription}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Workflow History */}
|
||||||
|
{currentTask.historyEvents && currentTask.historyEvents.length > 0 ? (
|
||||||
|
<CollapsibleTeamSection
|
||||||
|
title="Workflow History"
|
||||||
|
icon={<History size={14} />}
|
||||||
|
badge={currentTask.historyEvents.length}
|
||||||
|
contentClassName="pl-2.5"
|
||||||
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
|
headerContentClassName="pl-6"
|
||||||
|
defaultOpen={false}
|
||||||
|
>
|
||||||
|
<WorkflowTimeline events={currentTask.historyEvents} memberColorMap={colorMap} />
|
||||||
|
</CollapsibleTeamSection>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Comments */}
|
||||||
<CollapsibleTeamSection
|
<CollapsibleTeamSection
|
||||||
key={`task-logs:${currentTask.id}`}
|
title="Comments"
|
||||||
title="Execution Logs"
|
icon={<MessageSquare size={14} />}
|
||||||
icon={<ScrollText size={14} />}
|
badge={
|
||||||
headerExtra={
|
(currentTask.comments?.length ?? 0) > 0
|
||||||
logsRefreshing || executionPreviewOnline ? (
|
? (currentTask.comments?.length ?? 0)
|
||||||
<span className="flex items-center gap-2 text-[10px] text-[var(--color-text-muted)]">
|
: undefined
|
||||||
{executionPreviewOnline ? (
|
|
||||||
<span
|
|
||||||
className="pointer-events-none relative inline-flex size-2 shrink-0"
|
|
||||||
title="Online"
|
|
||||||
>
|
|
||||||
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-50" />
|
|
||||||
<span className="relative inline-flex size-2 rounded-full bg-emerald-400" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{logsRefreshing ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Loader2 size={10} className="animate-spin" />
|
|
||||||
Updating...
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
) : null
|
|
||||||
}
|
}
|
||||||
contentClassName="pl-2.5"
|
contentClassName="overflow-x-visible pl-0"
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
||||||
headerContentClassName="pl-6"
|
headerContentClassName="pl-6"
|
||||||
defaultOpen={false}
|
defaultOpen
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="pl-2.5">
|
||||||
<MemberLogsTab
|
<TaskCommentInput
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
taskId={currentTask.id}
|
taskId={currentTask.id}
|
||||||
taskOwner={currentTask.owner}
|
members={members}
|
||||||
taskStatus={currentTask.status}
|
replyTo={effectiveReplyTo}
|
||||||
taskWorkIntervals={currentTask.workIntervals}
|
onClearReply={clearReply}
|
||||||
taskSince={taskSince}
|
|
||||||
onRefreshingChange={setLogsRefreshing}
|
|
||||||
// Only show a "latest messages" preview when this task is owned by a subagent.
|
|
||||||
// For lead-owned tasks, the lead session is a mixed stream (lead + multiple agents),
|
|
||||||
// so filtering to "just the member messages" is unreliable and easy to mislead.
|
|
||||||
showSubagentPreview={Boolean(currentTask.owner) && !isLeadOwnedTask}
|
|
||||||
// Temporary debug option: for lead-owned tasks, show quick preview from lead session.
|
|
||||||
showLeadPreview={allowLeadExecutionPreview && isLeadOwnedTask}
|
|
||||||
onPreviewOnlineChange={setExecutionPreviewOnline}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTeamSection>
|
<TaskCommentsSection
|
||||||
) : null}
|
|
||||||
|
|
||||||
{blockedByIds.length > 0 ||
|
|
||||||
blocksIds.length > 0 ||
|
|
||||||
relatedIds.length > 0 ||
|
|
||||||
relatedByIds.length > 0 ||
|
|
||||||
kanbanTaskState ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* Dependencies */}
|
|
||||||
{blockedByIds.length > 0 ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<span className="inline-flex items-center gap-0.5 text-xs text-yellow-300">
|
|
||||||
<ArrowLeftFromLine size={12} />
|
|
||||||
Blocked by
|
|
||||||
</span>
|
|
||||||
{blockedByIds.map((id) => {
|
|
||||||
const depTask = taskMap.get(id);
|
|
||||||
const isCompleted = depTask?.status === 'completed';
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
|
||||||
isCompleted
|
|
||||||
? 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25'
|
|
||||||
: 'bg-yellow-500/15 text-yellow-300 hover:bg-yellow-500/25'
|
|
||||||
} cursor-pointer`}
|
|
||||||
title={
|
|
||||||
depTask
|
|
||||||
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
|
||||||
: `#${deriveTaskDisplayId(id)}`
|
|
||||||
}
|
|
||||||
onClick={() => handleDependencyClick(id)}
|
|
||||||
>
|
|
||||||
{depTask ? formatTaskDisplayLabel(depTask) : `#${deriveTaskDisplayId(id)}`}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{blocksIds.length > 0 ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<span className="inline-flex items-center gap-0.5 text-xs text-blue-400">
|
|
||||||
<ArrowRightFromLine size={12} />
|
|
||||||
Blocks
|
|
||||||
</span>
|
|
||||||
{blocksIds.map((id) => {
|
|
||||||
const depTask = taskMap.get(id);
|
|
||||||
const isCompleted = depTask?.status === 'completed';
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors ${
|
|
||||||
isCompleted
|
|
||||||
? 'bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/25'
|
|
||||||
: 'bg-blue-500/15 text-blue-400 hover:bg-blue-500/25'
|
|
||||||
} cursor-pointer`}
|
|
||||||
title={
|
|
||||||
depTask
|
|
||||||
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
|
||||||
: `#${deriveTaskDisplayId(id)}`
|
|
||||||
}
|
|
||||||
onClick={() => handleDependencyClick(id)}
|
|
||||||
>
|
|
||||||
{depTask ? formatTaskDisplayLabel(depTask) : `#${deriveTaskDisplayId(id)}`}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Review info */}
|
|
||||||
{kanbanTaskState ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{kanbanTaskState.reviewer ? (
|
|
||||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
|
||||||
Reviewer: {kanbanTaskState.reviewer}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{kanbanTaskState.errorDescription ? (
|
|
||||||
<span className="text-xs text-red-400">{kanbanTaskState.errorDescription}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Workflow History */}
|
|
||||||
{currentTask.historyEvents && currentTask.historyEvents.length > 0 ? (
|
|
||||||
<CollapsibleTeamSection
|
|
||||||
title="Workflow History"
|
|
||||||
icon={<History size={14} />}
|
|
||||||
badge={currentTask.historyEvents.length}
|
|
||||||
contentClassName="pl-2.5"
|
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
|
||||||
headerContentClassName="pl-6"
|
|
||||||
defaultOpen={false}
|
|
||||||
>
|
|
||||||
<WorkflowTimeline events={currentTask.historyEvents} memberColorMap={colorMap} />
|
|
||||||
</CollapsibleTeamSection>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Comments */}
|
|
||||||
<CollapsibleTeamSection
|
|
||||||
title="Comments"
|
|
||||||
icon={<MessageSquare size={14} />}
|
|
||||||
badge={
|
|
||||||
(currentTask.comments?.length ?? 0) > 0
|
|
||||||
? (currentTask.comments?.length ?? 0)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
contentClassName="overflow-x-visible pl-0"
|
|
||||||
headerClassName="-mx-6 w-[calc(100%+3rem)]"
|
|
||||||
headerContentClassName="pl-6"
|
|
||||||
defaultOpen
|
|
||||||
>
|
|
||||||
<div className="pl-2.5">
|
|
||||||
<TaskCommentInput
|
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
taskId={currentTask.id}
|
taskId={currentTask.id}
|
||||||
|
comments={currentTask.comments ?? []}
|
||||||
members={members}
|
members={members}
|
||||||
replyTo={effectiveReplyTo}
|
hideHeader
|
||||||
onClearReply={clearReply}
|
hideInput
|
||||||
|
onReply={handleReply}
|
||||||
|
onTaskIdClick={
|
||||||
|
onScrollToTask ? (taskId) => handleDependencyClick(taskId) : undefined
|
||||||
|
}
|
||||||
|
containerClassName="-mx-6"
|
||||||
|
unreadCommentIds={unreadSnapshotRef.current}
|
||||||
/>
|
/>
|
||||||
</div>
|
</CollapsibleTeamSection>
|
||||||
<TaskCommentsSection
|
</div>
|
||||||
teamName={teamName}
|
|
||||||
taskId={currentTask.id}
|
|
||||||
comments={currentTask.comments ?? []}
|
|
||||||
members={members}
|
|
||||||
hideHeader
|
|
||||||
hideInput
|
|
||||||
onReply={handleReply}
|
|
||||||
onTaskIdClick={onScrollToTask ? (taskId) => handleDependencyClick(taskId) : undefined}
|
|
||||||
containerClassName="-mx-6"
|
|
||||||
unreadCommentIds={unreadSnapshotRef.current}
|
|
||||||
/>
|
|
||||||
</CollapsibleTeamSection>
|
|
||||||
</LightboxLockProvider>
|
</LightboxLockProvider>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export const KanbanColumn = ({
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,12 @@ export const MessageComposer = ({
|
||||||
// Track whether we initiated a send — clear draft only on confirmed success
|
// Track whether we initiated a send — clear draft only on confirmed success
|
||||||
const pendingSendRef = useRef(false);
|
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(() => {
|
const handleSend = useCallback(() => {
|
||||||
if (!canSend) return;
|
if (!canSend) return;
|
||||||
dismissMentionsRef.current?.();
|
dismissMentionsRef.current?.();
|
||||||
|
|
@ -835,6 +841,7 @@ export const MessageComposer = ({
|
||||||
projectPath={projectPath}
|
projectPath={projectPath}
|
||||||
onFileChipInsert={draft.addChip}
|
onFileChipInsert={draft.addChip}
|
||||||
onModEnter={handleSend}
|
onModEnter={handleSend}
|
||||||
|
onShiftTab={handleCycleActionMode}
|
||||||
dismissMentionsRef={dismissMentionsRef}
|
dismissMentionsRef={dismissMentionsRef}
|
||||||
minRows={2}
|
minRows={2}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,12 @@ export const MentionSuggestionList = ({
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{s.subtitle && !isTask ? (
|
{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}
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -287,9 +287,9 @@ function parseSegments(
|
||||||
// Default fallback color for mentions without a team color
|
// Default fallback color for mentions without a team color
|
||||||
const DEFAULT_MENTION_BG = 'rgba(59, 130, 246, 0.15)';
|
const DEFAULT_MENTION_BG = 'rgba(59, 130, 246, 0.15)';
|
||||||
const DEFAULT_MENTION_TEXT = '#60a5fa';
|
const DEFAULT_MENTION_TEXT = '#60a5fa';
|
||||||
const URL_BADGE_BG = 'rgba(37, 99, 235, 0.12)';
|
const URL_BADGE_BG = 'var(--url-badge-bg)';
|
||||||
const URL_BADGE_BORDER = 'rgba(96, 165, 250, 0.22)';
|
const URL_BADGE_BORDER = 'var(--url-badge-border)';
|
||||||
const URL_BADGE_TEXT = '#bfdbfe';
|
const URL_BADGE_TEXT = 'var(--url-badge-text)';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Component
|
// Component
|
||||||
|
|
@ -324,6 +324,8 @@ interface MentionableTextareaProps extends Omit<
|
||||||
taskSuggestions?: MentionSuggestion[];
|
taskSuggestions?: MentionSuggestion[];
|
||||||
/** Called when Enter (without Shift) is pressed. */
|
/** Called when Enter (without Shift) is pressed. */
|
||||||
onModEnter?: () => void;
|
onModEnter?: () => void;
|
||||||
|
/** Called when Shift+Tab is pressed. */
|
||||||
|
onShiftTab?: () => void;
|
||||||
/** Ref that receives the dismiss callback to close mention dropdown from outside */
|
/** Ref that receives the dismiss callback to close mention dropdown from outside */
|
||||||
dismissMentionsRef?: React.MutableRefObject<(() => void) | null>;
|
dismissMentionsRef?: React.MutableRefObject<(() => void) | null>;
|
||||||
}
|
}
|
||||||
|
|
@ -346,6 +348,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
teamSuggestions = [],
|
teamSuggestions = [],
|
||||||
taskSuggestions = [],
|
taskSuggestions = [],
|
||||||
onModEnter,
|
onModEnter,
|
||||||
|
onShiftTab,
|
||||||
dismissMentionsRef,
|
dismissMentionsRef,
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
|
|
@ -823,6 +826,12 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
});
|
});
|
||||||
if (e.defaultPrevented) return;
|
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
|
// Enter (without Shift) → submit; Shift+Enter → newline
|
||||||
if (e.key === 'Enter' && !e.shiftKey && onModEnter) {
|
if (e.key === 'Enter' && !e.shiftKey && onModEnter) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -834,6 +843,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
onModEnter,
|
onModEnter,
|
||||||
|
onShiftTab,
|
||||||
handleChipKeyDown,
|
handleChipKeyDown,
|
||||||
mentionHandleKeyDown,
|
mentionHandleKeyDown,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
@ -1030,14 +1040,14 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
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={{
|
style={{
|
||||||
backgroundColor: URL_BADGE_BG,
|
backgroundColor: URL_BADGE_BG,
|
||||||
color: URL_BADGE_TEXT,
|
color: URL_BADGE_TEXT,
|
||||||
|
borderRadius: '4px',
|
||||||
boxShadow: `inset 0 0 0 1px ${URL_BADGE_BORDER}`,
|
boxShadow: `inset 0 0 0 1px ${URL_BADGE_BORDER}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="truncate">{seg.value}</span>
|
{seg.value}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,12 @@ const badgeVariants = cva(
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
|
extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
const Badge = ({ className, variant, ...props }: BadgeProps): React.JSX.Element => {
|
const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
|
||||||
return <span className={cn(badgeVariants({ variant }), className)} {...props} />;
|
({ 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
|
// eslint-disable-next-line react-refresh/only-export-components -- Standard shadcn export pattern
|
||||||
export { Badge, badgeVariants };
|
export { Badge, badgeVariants };
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,10 @@
|
||||||
/* Inline code */
|
/* Inline code */
|
||||||
--inline-code-bg: rgba(148, 163, 184, 0.08);
|
--inline-code-bg: rgba(148, 163, 184, 0.08);
|
||||||
--inline-code-text: #e2e8f0;
|
--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 viewer */
|
||||||
--diff-added-bg: rgba(34, 197, 94, 0.15);
|
--diff-added-bg: rgba(34, 197, 94, 0.15);
|
||||||
--diff-added-text: #4ade80;
|
--diff-added-text: #4ade80;
|
||||||
|
|
@ -440,6 +444,10 @@
|
||||||
/* Inline code - Warm neutral */
|
/* Inline code - Warm neutral */
|
||||||
--inline-code-bg: rgba(0, 0, 0, 0.05);
|
--inline-code-bg: rgba(0, 0, 0, 0.05);
|
||||||
--inline-code-text: #3a3935;
|
--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 viewer - Light mode */
|
||||||
--diff-added-bg: rgba(34, 197, 94, 0.18);
|
--diff-added-bg: rgba(34, 197, 94, 0.18);
|
||||||
--diff-added-text: #14532d;
|
--diff-added-text: #14532d;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue