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:
parent
c2d0a20811
commit
bd96e3672b
10 changed files with 130 additions and 27 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue