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
|
#!/usr/bin/env node
|
||||||
|
import { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
import { FastMCP } from 'fastmcp';
|
import { FastMCP } from 'fastmcp';
|
||||||
|
|
||||||
import { registerTools } from './tools';
|
import { registerTools } from './tools';
|
||||||
|
|
@ -14,7 +16,7 @@ export function createServer() {
|
||||||
return server;
|
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();
|
const server = createServer();
|
||||||
void server.start({
|
void server.start({
|
||||||
transportType: 'stdio',
|
transportType: 'stdio',
|
||||||
|
|
|
||||||
|
|
@ -999,6 +999,7 @@ export class TeamDataService {
|
||||||
): Promise<TaskComment> {
|
): Promise<TaskComment> {
|
||||||
const controller = this.getController(teamName);
|
const controller = this.getController(teamName);
|
||||||
const addResult = controller.tasks.addTaskComment(taskId, {
|
const addResult = controller.tasks.addTaskComment(taskId, {
|
||||||
|
from: 'user',
|
||||||
text,
|
text,
|
||||||
attachments,
|
attachments,
|
||||||
}) as { task?: TeamTask; comment?: TaskComment };
|
}) 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>" }
|
{ 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.
|
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.
|
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.
|
- 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).
|
- 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.
|
- 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:
|
- Typical flow:
|
||||||
a) Owner finishes work on #X -> task_complete #X
|
a) Owner finishes work on #X -> task_complete #X
|
||||||
b) Reviewer accepts -> review_approve #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:
|
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:
|
a) STEP 1 — FIRST, set the clarification flag with MCP tool task_set_clarification:
|
||||||
{ teamName: "${teamName}", taskId: "<taskId>", value: "lead" }
|
{ 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:
|
If the lead replies via SendMessage instead, clear the flag yourself once you have the answer:
|
||||||
{ teamName: "${teamName}", taskId: "<taskId>", value: "clear" }
|
{ teamName: "${teamName}", taskId: "<taskId>", value: "clear" }
|
||||||
d) Do NOT set clarification to "user" yourself — only the team lead escalates to the user.
|
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 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.
|
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.`);
|
Failure to follow this protocol means the task board will show incorrect status.`);
|
||||||
|
|
@ -677,6 +678,7 @@ ${AGENT_BLOCK_CLOSE}
|
||||||
- any node/bash commands
|
- any node/bash commands
|
||||||
- internal file paths (~/.claude/teams/, etc.)
|
- internal file paths (~/.claude/teams/, etc.)
|
||||||
- instructions to run commands in terminal
|
- 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.
|
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.").`;
|
- 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);
|
const prompt = buildProvisioningPrompt(request);
|
||||||
let child: ReturnType<typeof spawn>;
|
let child: ReturnType<typeof spawn>;
|
||||||
const { env: shellEnv } = await this.buildProvisioningEnv();
|
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 = [
|
const spawnArgs = [
|
||||||
'--input-format',
|
'--input-format',
|
||||||
'stream-json',
|
'stream-json',
|
||||||
|
|
@ -2188,7 +2197,15 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
let child: ReturnType<typeof spawn>;
|
let child: ReturnType<typeof spawn>;
|
||||||
const { env: shellEnv } = await this.buildProvisioningEnv();
|
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 = [
|
const launchArgs = [
|
||||||
'--input-format',
|
'--input-format',
|
||||||
'stream-json',
|
'stream-json',
|
||||||
|
|
|
||||||
|
|
@ -955,7 +955,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<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={
|
style={
|
||||||
headerColorSet
|
headerColorSet
|
||||||
? { borderLeftWidth: '3px', borderLeftColor: headerColorSet.border }
|
? { borderLeftWidth: '3px', borderLeftColor: headerColorSet.border }
|
||||||
|
|
@ -964,7 +964,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
>
|
>
|
||||||
{headerColorSet ? (
|
{headerColorSet ? (
|
||||||
<div
|
<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) }}
|
style={{ backgroundColor: getThemedBadge(headerColorSet, isLight) }}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : 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 { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
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 { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
|
|
||||||
import type { ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
|
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">
|
<div className="flex items-center gap-2 px-3 py-2">
|
||||||
<span className="relative flex size-2 shrink-0">
|
<span className="relative inline-flex shrink-0">
|
||||||
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-70" />
|
<img
|
||||||
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
|
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>
|
</span>
|
||||||
<Loader2
|
|
||||||
className="size-3.5 shrink-0 animate-spin"
|
|
||||||
style={{ color: colors.border }}
|
|
||||||
/>
|
|
||||||
{onMemberClick ? (
|
{onMemberClick ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ export const AddMemberDialog = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Add Member</DialogTitle>
|
<DialogTitle>Add Member</DialogTitle>
|
||||||
<DialogDescription>Add a new member to {teamName}</DialogDescription>
|
<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);
|
setMergedIndex((prev) => (prev - 1 + allSuggestions.length) % allSuggestions.length);
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
e.preventDefault();
|
if (!e.shiftKey) {
|
||||||
if (allSuggestions[mergedIndex]) {
|
e.preventDefault();
|
||||||
handleMergedSelect(allSuggestions[mergedIndex]);
|
if (allSuggestions[mergedIndex]) {
|
||||||
|
handleMergedSelect(allSuggestions[mergedIndex]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
|
@ -502,9 +504,18 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
[isOpen, allSuggestions, mergedIndex, handleMergedSelect, dismiss]
|
[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(
|
const composedHandleKeyDown = React.useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
(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
|
// 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();
|
||||||
|
|
@ -512,7 +523,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleChipKeyDown(e);
|
handleChipKeyDown(e);
|
||||||
if (!e.defaultPrevented) {
|
if (!e.defaultPrevented && !isOpen) {
|
||||||
if (enableFiles) {
|
if (enableFiles) {
|
||||||
fileMentionHandleKeyDown(e);
|
fileMentionHandleKeyDown(e);
|
||||||
} else {
|
} 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 ---
|
// --- Chip reconciliation on text change ---
|
||||||
|
|
|
||||||
|
|
@ -298,8 +298,10 @@ export function useMentionDetection({
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'Enter':
|
case 'Enter':
|
||||||
e.preventDefault();
|
if (!e.shiftKey) {
|
||||||
selectSuggestion(filteredSuggestions[selectedIndex]);
|
e.preventDefault();
|
||||||
|
selectSuggestion(filteredSuggestions[selectedIndex]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -233,7 +233,7 @@
|
||||||
/* Light theme overrides - Warm neutral palette for eye comfort */
|
/* Light theme overrides - Warm neutral palette for eye comfort */
|
||||||
:root.light {
|
:root.light {
|
||||||
--color-surface: #f9f9f7; /* Warm off-white (not pure white) */
|
--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-overlay: #e8e7e4; /* Warm overlay */
|
||||||
--color-surface-sidebar: #f1f0ee; /* Warm sidebar, distinct from main */
|
--color-surface-sidebar: #f1f0ee; /* Warm sidebar, distinct from main */
|
||||||
--color-border: #d5d3cf; /* Warm neutral border */
|
--color-border: #d5d3cf; /* Warm neutral border */
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,65 @@ describe('TeamDataService', () => {
|
||||||
await expect(service.reconcileTeamArtifacts('my-team')).rejects.toThrow('reconcile failed');
|
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 () => {
|
it('includes projectPath from config when creating a task', async () => {
|
||||||
const createTaskMock = vi.fn((task) => task);
|
const createTaskMock = vi.fn((task) => task);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue