fix: enhance task comment functionality and improve UI interactions

- Updated the addTaskComment method to include the author as 'user' for better context in task comments.
- Enhanced error handling in TeamProvisioningService during MCP config file writing to ensure proper cleanup on failure.
- Improved user interface elements in TeamDetailView and AddMemberDialog for better accessibility and responsiveness.
- Refined mention handling in MentionableTextarea to allow for smoother user interactions with task references.
- Adjusted CSS styles for better visual contrast in light theme, enhancing overall user experience.
This commit is contained in:
iliya 2026-03-07 21:22:49 +02:00
parent c2d0a20811
commit bd96e3672b
10 changed files with 130 additions and 27 deletions

View file

@ -1,4 +1,6 @@
#!/usr/bin/env node
import { pathToFileURL } from 'node:url';
import { FastMCP } from 'fastmcp';
import { registerTools } from './tools';
@ -14,7 +16,7 @@ export function createServer() {
return server;
}
if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const server = createServer();
void server.start({
transportType: 'stdio',

View file

@ -999,6 +999,7 @@ export class TeamDataService {
): Promise<TaskComment> {
const controller = this.getController(teamName);
const addResult = controller.tasks.addTaskComment(taskId, {
from: 'user',
text,
attachments,
}) as { task?: TeamTask; comment?: TaskComment };

View file

@ -477,14 +477,15 @@ function buildTaskStatusProtocol(teamName: string): string {
{ teamName: "${teamName}", taskId: "<taskId>", text: "<summary of your finding or decision>", from: "<your-name>" }
Do NOT comment on trivial coordination messages. Only comment when the information is valuable context for the task.
8. When sending a message about a specific task, include its short display label like #<displayId> in your SendMessage summary field for traceability.
9. Review workflow clarity (IMPORTANT):
9. In ALL human-facing or teammate-facing message text, when you mention a task reference, ALWAYS write it with a leading # (for example: #abcd1234, not abcd1234 or "task abcd1234").
10. Review workflow clarity (IMPORTANT):
- The work task (e.g. #1) is the thing that must end up APPROVED after review.
- If you are reviewing work for task #X, run review_approve/review_request_changes on #X (the work task).
- Do NOT approve a separate "review task" (e.g. #2 created just to ask for a review) that will put the wrong task into APPROVED.
- Typical flow:
a) Owner finishes work on #X -> task_complete #X
b) Reviewer accepts -> review_approve #X
10. CLARIFICATION PROTOCOL (CRITICAL MANDATORY):
11. CLARIFICATION PROTOCOL (CRITICAL MANDATORY):
When you are blocked and need information to continue a task, you MUST do BOTH steps below skipping the MCP update breaks the task board:
a) STEP 1 FIRST, set the clarification flag with MCP tool task_set_clarification:
{ teamName: "${teamName}", taskId: "<taskId>", value: "lead" }
@ -494,7 +495,7 @@ function buildTaskStatusProtocol(teamName: string): string {
If the lead replies via SendMessage instead, clear the flag yourself once you have the answer:
{ teamName: "${teamName}", taskId: "<taskId>", value: "clear" }
d) Do NOT set clarification to "user" yourself only the team lead escalates to the user.
11. DEPENDENCY AWARENESS:
12. DEPENDENCY AWARENESS:
When your task has blockedBy dependencies, check if they are completed before starting.
When you complete a task that blocks others, mention this in your completion message so blocked teammates can proceed.
Failure to follow this protocol means the task board will show incorrect status.`);
@ -677,6 +678,7 @@ ${AGENT_BLOCK_CLOSE}
- any node/bash commands
- internal file paths (~/.claude/teams/, etc.)
- instructions to run commands in terminal
- task references without a leading # (for example write #abcd1234, not abcd1234)
Instead, describe the action in human-friendly language (e.g. "Task #6 is complete." instead of showing a command to mark it complete). If you need to update task status, do it YOURSELF never ask the user to run a command.
- CRITICAL: When processing relayed inbox messages, your text output is shown to the user. Do NOT wrap your entire response in an agent-only block. If you need agent-only instructions, put them in a separate block and include a brief human-readable summary outside of it (e.g. "Delegated task to carol." or "Acknowledged, no action needed.").`;
}
@ -1855,7 +1857,14 @@ export class TeamProvisioningService {
const prompt = buildProvisioningPrompt(request);
let child: ReturnType<typeof spawn>;
const { env: shellEnv } = await this.buildProvisioningEnv();
const mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile();
let mcpConfigPath: string;
try {
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile();
} catch (error) {
this.runs.delete(runId);
this.activeByTeam.delete(request.teamName);
throw error;
}
const spawnArgs = [
'--input-format',
'stream-json',
@ -2188,7 +2197,15 @@ export class TeamProvisioningService {
);
let child: ReturnType<typeof spawn>;
const { env: shellEnv } = await this.buildProvisioningEnv();
const mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile();
let mcpConfigPath: string;
try {
mcpConfigPath = await this.mcpConfigBuilder.writeConfigFile();
} catch (error) {
this.runs.delete(runId);
this.activeByTeam.delete(request.teamName);
await this.restorePrelaunchConfig(request.teamName);
throw error;
}
const launchArgs = [
'--input-format',
'stream-json',

View file

@ -955,7 +955,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
)}
<div
className="relative mb-3 overflow-hidden rounded-lg border border-[var(--color-border)] px-4 py-3"
className="relative -mx-4 -mt-4 mb-3 overflow-hidden border-b border-[var(--color-border)] px-4 py-3"
style={
headerColorSet
? { borderLeftWidth: '3px', borderLeftColor: headerColorSet.border }
@ -964,7 +964,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
>
{headerColorSet ? (
<div
className="pointer-events-none absolute inset-0 z-0 rounded-lg"
className="pointer-events-none absolute inset-0 z-0"
style={{ backgroundColor: getThemedBadge(headerColorSet, isLight) }}
/>
) : null}

View file

@ -2,9 +2,8 @@ import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { agentAvatarUrl, buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
import { Loader2 } from 'lucide-react';
import type { ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
@ -51,14 +50,18 @@ export const ActiveTasksBlock = ({
}}
>
<div className="flex items-center gap-2 px-3 py-2">
<span className="relative flex size-2 shrink-0">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
<span className="relative inline-flex shrink-0">
<img
src={agentAvatarUrl(member.name, 24)}
alt=""
className="size-5 rounded-full bg-[var(--color-surface-raised)]"
loading="lazy"
/>
<span className="absolute -bottom-0.5 -right-0.5 flex h-2.5 w-2.5">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
<span className="relative inline-flex size-full rounded-full bg-emerald-500" />
</span>
</span>
<Loader2
className="size-3.5 shrink-0 animate-spin"
style={{ color: colors.border }}
/>
{onMemberClick ? (
<button
type="button"

View file

@ -126,7 +126,7 @@ export const AddMemberDialog = ({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Add Member</DialogTitle>
<DialogDescription>Add a new member to {teamName}</DialogDescription>

View file

@ -488,9 +488,11 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
setMergedIndex((prev) => (prev - 1 + allSuggestions.length) % allSuggestions.length);
break;
case 'Enter':
e.preventDefault();
if (allSuggestions[mergedIndex]) {
handleMergedSelect(allSuggestions[mergedIndex]);
if (!e.shiftKey) {
e.preventDefault();
if (allSuggestions[mergedIndex]) {
handleMergedSelect(allSuggestions[mergedIndex]);
}
}
break;
case 'Escape':
@ -502,9 +504,18 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
[isOpen, allSuggestions, mergedIndex, handleMergedSelect, dismiss]
);
// Composed key handler: Mod+Enter submit → chip logic → mention logic
// Composed key handler: mention logic first (when open) → Mod+Enter submit → chip logic → mention fallback
const composedHandleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// When mention dropdown is open, let mention handler consume Enter/Arrow keys first
if (isOpen && effectiveSuggestions.length > 0) {
if (enableFiles) {
fileMentionHandleKeyDown(e);
} else {
mentionHandleKeyDown(e);
}
if (e.defaultPrevented) return;
}
// Enter (without Shift) → submit; Shift+Enter → newline
if (e.key === 'Enter' && !e.shiftKey && onModEnter) {
e.preventDefault();
@ -512,7 +523,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
return;
}
handleChipKeyDown(e);
if (!e.defaultPrevented) {
if (!e.defaultPrevented && !isOpen) {
if (enableFiles) {
fileMentionHandleKeyDown(e);
} else {
@ -520,7 +531,15 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
}
}
},
[onModEnter, handleChipKeyDown, enableFiles, fileMentionHandleKeyDown, mentionHandleKeyDown]
[
onModEnter,
handleChipKeyDown,
enableFiles,
fileMentionHandleKeyDown,
mentionHandleKeyDown,
isOpen,
effectiveSuggestions.length,
]
);
// --- Chip reconciliation on text change ---

View file

@ -298,8 +298,10 @@ export function useMentionDetection({
);
break;
case 'Enter':
e.preventDefault();
selectSuggestion(filteredSuggestions[selectedIndex]);
if (!e.shiftKey) {
e.preventDefault();
selectSuggestion(filteredSuggestions[selectedIndex]);
}
break;
case 'Escape':
e.preventDefault();

View file

@ -233,7 +233,7 @@
/* Light theme overrides - Warm neutral palette for eye comfort */
:root.light {
--color-surface: #f9f9f7; /* Warm off-white (not pure white) */
--color-surface-raised: #f0efed; /* Warm raised surface, clearly distinct */
--color-surface-raised: #e8e6e3; /* Warm raised surface — stronger contrast for visible hover */
--color-surface-overlay: #e8e7e4; /* Warm overlay */
--color-surface-sidebar: #f1f0ee; /* Warm sidebar, distinct from main */
--color-border: #d5d3cf; /* Warm neutral border */

View file

@ -118,6 +118,65 @@ describe('TeamDataService', () => {
await expect(service.reconcileTeamArtifacts('my-team')).rejects.toThrow('reconcile failed');
});
it('writes UI task comments with author user', async () => {
const addTaskComment = vi.fn(() => ({
comment: {
id: 'comment-1',
author: 'user',
text: 'Need clarification',
createdAt: '2026-03-07T20:00:00.000Z',
type: 'regular',
},
task: {
id: 'task-1',
subject: 'Investigate',
status: 'pending',
owner: 'team-lead',
},
}));
const service = new TeamDataService(
{
listTeams: vi.fn(),
getConfig: vi.fn(async () => ({ name: 'My team', members: [{ name: 'team-lead', role: 'Lead' }] })),
} as never,
{
getTasks: vi.fn(async () => []),
} as never,
{
listInboxNames: vi.fn(async () => []),
getMessages: vi.fn(async () => []),
} as never,
{} as never,
{} as never,
{
resolveMembers: vi.fn(() => []),
} as never,
{
getState: vi.fn(async () => ({ teamName: 'my-team', reviewers: [], tasks: {} })),
garbageCollect: vi.fn(async () => undefined),
} as never,
{} as never,
{} as never,
{} as never,
() =>
({
tasks: {
addTaskComment,
setNeedsClarification: vi.fn(),
},
}) as never
);
await service.addTaskComment('my-team', 'task-1', 'Need clarification');
expect(addTaskComment).toHaveBeenCalledWith('task-1', {
from: 'user',
text: 'Need clarification',
attachments: undefined,
});
});
it('includes projectPath from config when creating a task', async () => {
const createTaskMock = vi.fn((task) => task);