Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@

## Changed

- **Chief of Staff feedback now captures why an agent helped or missed the mark.** Add detail to positive, negative, or neutral ratings so future CoS learning has the context needed to improve.

- **Branch reconciler prioritizes recognized work branches.** The in-flight set handed to the coordinator agent is now ordered by branch prefix — `claim/`, `cos/`, `next/`, `feature/`, `fix/`, and other conventional prefixes are reconciled ahead of unrecognized/ad-hoc branches — so a bounded run spends its budget on real deliverables first. Both `/` and `-` separators match.
74 changes: 49 additions & 25 deletions client/src/components/cos/tabs/AgentCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default function AgentCard({ agent, onPause, onKill, onDelete, onResume,
}, [agent.btwMessages]);
const [submittingFeedback, setSubmittingFeedback] = useState(false);
const [showFeedbackComment, setShowFeedbackComment] = useState(false);
const [feedbackComment, setFeedbackComment] = useState('');
const [feedbackComment, setFeedbackComment] = useState(agent.feedback?.comment || '');

// Determine if this is a system agent (health check, etc.)
const isSystemAgent = agent.taskId?.startsWith('sys-') || agent.id?.startsWith('sys-');
Expand All @@ -161,7 +161,7 @@ export default function AgentCard({ agent, onPause, onKill, onDelete, onResume,
if (result?.success) {
setFeedbackState(rating);
setShowFeedbackComment(false);
setFeedbackComment('');
setFeedbackComment(result.agent?.feedback?.comment || '');
toast.success(`Feedback recorded: ${rating}`);
onFeedbackChange?.(result.agent);
}
Expand Down Expand Up @@ -840,42 +840,66 @@ export default function AgentCard({ agent, onPause, onKill, onDelete, onResume,
>
<ThumbsDown size={14} />
</button>
{!feedbackState && (
<button
onClick={() => setShowFeedbackComment(!showFeedbackComment)}
className="p-1.5 rounded text-gray-500 hover:text-white hover:bg-port-border/50 transition-colors min-w-[32px] min-h-[32px] flex items-center justify-center"
title="Add comment"
aria-label="Add feedback comment"
aria-expanded={showFeedbackComment}
>
<MessageSquare size={14} />
</button>
)}
<button
onClick={() => setShowFeedbackComment(!showFeedbackComment)}
className="p-1.5 rounded text-gray-500 hover:text-white hover:bg-port-border/50 transition-colors min-w-[32px] min-h-[32px] flex items-center justify-center"
title={feedbackState ? 'Add feedback detail' : 'Add feedback comment'}
aria-label={feedbackState ? 'Add feedback detail' : 'Add feedback comment'}
aria-expanded={showFeedbackComment}
>
<MessageSquare size={14} />
</button>
</div>
{feedbackState && (
<span className={`text-xs ${feedbackState === 'positive' ? 'text-port-success' : 'text-port-error'}`}>
{feedbackState === 'positive' ? 'Thanks for the feedback!' : 'We\'ll improve'}
<span className={`text-xs ${feedbackState === 'positive' ? 'text-port-success' : feedbackState === 'negative' ? 'text-port-error' : 'text-gray-400'}`}>
{feedbackState === 'positive' ? 'Thanks for the feedback!' : feedbackState === 'negative' ? 'We\'ll improve' : 'Feedback recorded'}
</span>
)}
</div>
{/* Comment input */}
{showFeedbackComment && !feedbackState && (
<div className="mt-2 flex gap-2">
{showFeedbackComment && (
<div className="mt-2 flex flex-col gap-2 sm:flex-row">
<input
type="text"
value={feedbackComment}
onChange={(e) => setFeedbackComment(e.target.value)}
placeholder="Optional: add a comment..."
placeholder="What made this work well or poorly?"
className="flex-1 px-2 py-1 text-sm bg-port-bg border border-port-border rounded text-white placeholder-gray-500 focus:outline-hidden focus:border-port-accent min-h-[32px]"
maxLength={200}
/>
<button
onClick={() => submitFeedback('neutral')}
disabled={submittingFeedback || !feedbackComment.trim()}
className="px-3 py-1 text-xs bg-port-accent hover:bg-port-accent/80 text-white rounded disabled:opacity-50 min-h-[32px]"
>
Submit
</button>
{feedbackState ? (
<button
onClick={() => submitFeedback(feedbackState)}
disabled={submittingFeedback || !feedbackComment.trim()}
className="px-3 py-1 text-xs bg-port-accent hover:bg-port-accent/80 text-white rounded disabled:opacity-50 min-h-[32px]"
>
Save detail
</button>
) : (
<div className="flex flex-wrap gap-1">
<button
onClick={() => submitFeedback('positive')}
disabled={submittingFeedback || !feedbackComment.trim()}
className="px-2 py-1 text-xs text-port-success border border-port-success/40 hover:bg-port-success/10 rounded disabled:opacity-50 min-h-[32px]"
>
Helpful
</button>
<button
onClick={() => submitFeedback('negative')}
disabled={submittingFeedback || !feedbackComment.trim()}
className="px-2 py-1 text-xs text-port-error border border-port-error/40 hover:bg-port-error/10 rounded disabled:opacity-50 min-h-[32px]"
>
Needs work
</button>
<button
onClick={() => submitFeedback('neutral')}
disabled={submittingFeedback || !feedbackComment.trim()}
className="px-2 py-1 text-xs text-gray-300 border border-port-border hover:bg-port-border/50 rounded disabled:opacity-50 min-h-[32px]"
>
Neutral
</button>
</div>
)}
</div>
)}
</div>
Expand Down
58 changes: 58 additions & 0 deletions client/src/components/cos/tabs/AgentCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,62 @@ describe('AgentCard feedback', () => {
);
await waitFor(() => expect(onFeedbackChange).toHaveBeenCalledWith(updatedAgent));
});

it('records a negative rating with the detail needed to improve future work', async () => {
const user = userEvent.setup();
const updatedAgent = {
...agent,
feedback: {
rating: 'negative',
comment: 'The implementation did not include tests.',
submittedAt: '2026-07-13T12:00:00.000Z'
}
};
api.submitCosAgentFeedback.mockResolvedValue({ success: true, agent: updatedAgent });

render(
<MemoryRouter>
<AgentCard agent={agent} completed />
</MemoryRouter>
);

await user.click(screen.getByRole('button', { name: 'Add feedback comment' }));
await user.type(screen.getByPlaceholderText('What made this work well or poorly?'), updatedAgent.feedback.comment);
await user.click(screen.getByRole('button', { name: 'Needs work' }));

expect(api.submitCosAgentFeedback).toHaveBeenCalledWith(
agent.id,
{ rating: 'negative', comment: updatedAgent.feedback.comment },
{ silent: true }
);
});

it('lets a quick rating receive a follow-up detail', async () => {
const user = userEvent.setup();
const ratedAgent = {
...agent,
feedback: { rating: 'positive', submittedAt: '2026-07-13T12:00:00.000Z' }
};
const detailedAgent = {
...ratedAgent,
feedback: { ...ratedAgent.feedback, comment: 'The focused test coverage was useful.' }
};
api.submitCosAgentFeedback.mockResolvedValue({ success: true, agent: detailedAgent });

render(
<MemoryRouter>
<AgentCard agent={ratedAgent} completed />
</MemoryRouter>
);

await user.click(screen.getByRole('button', { name: 'Add feedback detail' }));
await user.type(screen.getByPlaceholderText('What made this work well or poorly?'), detailedAgent.feedback.comment);
await user.click(screen.getByRole('button', { name: 'Save detail' }));

expect(api.submitCosAgentFeedback).toHaveBeenCalledWith(
agent.id,
{ rating: 'positive', comment: detailedAgent.feedback.comment },
{ silent: true }
);
});
});
2 changes: 1 addition & 1 deletion docs/features/chief-of-staff.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Autonomous agent manager that watches task files, spawns sub-agents, and maintai
6. **Self-Improvement**: Can analyze performance and suggest prompt/config improvements
7. **Script Generation**: Creates automation scripts for repetitive tasks
8. **Report Generation**: Daily summaries of completed work
9. **Durable Agent Feedback**: Completion notifications accept quick ratings, while the Agents tab keeps a filterable queue of loaded runs that still need feedback after a notification expires
9. **Durable Agent Feedback**: Completion notifications accept quick ratings, while the Agents tab keeps a filterable queue of loaded runs that still need feedback after a notification expires. Feedback details can be attached to helpful, unhelpful, or neutral ratings so learning has actionable context.

## Task File Format

Expand Down