diff --git a/src/main/services/team/TeamAgentToolsInstaller.ts b/src/main/services/team/TeamAgentToolsInstaller.ts index 210a8dfa..739bc6f3 100644 --- a/src/main/services/team/TeamAgentToolsInstaller.ts +++ b/src/main/services/team/TeamAgentToolsInstaller.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { atomicWriteAsync } from './atomicWrite'; const TOOL_FILE_NAME = 'teamctl.js'; -const TOOL_VERSION = 8; +const TOOL_VERSION = 9; function buildTeamCtlScript(): string { const script = String.raw`#!/usr/bin/env node @@ -201,6 +201,17 @@ function setTaskStatus(paths, taskId, status) { writeTask(taskPath, task); } +function setTaskOwner(paths, taskId, owner) { + const { taskPath, task } = readTask(paths, taskId); + if (owner) { + task.owner = owner; + } else { + delete task.owner; + } + writeTask(taskPath, task); + return task; +} + function addTaskComment(paths, taskId, flags) { var text = typeof flags.text === 'string' ? flags.text.trim() : ''; if (!text) die('Missing --text'); @@ -655,6 +666,7 @@ function printHelp() { ' node teamctl.js task complete [--team ]', ' node teamctl.js task start [--team ]', ' node teamctl.js task create --subject "..." [--description "..."] [--prompt "..."] [--owner "member"] [--status pending|in_progress|completed|deleted] [--notify --from "member"] [--team ]', + ' node teamctl.js task set-owner [--notify --from "member"] [--team ]', ' node teamctl.js task comment --text "..." [--from "member"] [--team ]', ' node teamctl.js task set-clarification [--from "member"] [--team ]', ' node teamctl.js task briefing --for [--team ]', @@ -791,6 +803,37 @@ async function main() { process.stdout.write('OK task #' + String(id) + ' needsClarification=' + (val === 'clear' ? 'cleared' : String(val)) + '\n'); return; } + if (action === 'set-owner' || action === 'assign') { + const id = rest[0] || args.flags.id; + const owner = rest[1] || args.flags.owner; + if (!id) die('Usage: task set-owner '); + if (!owner) die('Usage: task set-owner '); + const effectiveOwner = owner === 'clear' || owner === 'none' ? null : String(owner); + const task = setTaskOwner(paths, String(id), effectiveOwner); + process.stdout.write('OK task #' + String(id) + ' owner=' + (effectiveOwner || 'cleared') + '\n'); + const notify = args.flags.notify === true; + if (notify && effectiveOwner) { + const from = typeof args.flags.from === 'string' && args.flags.from.trim() ? args.flags.from.trim() : inferLeadName(paths); + const parts = ['Task assigned to you: #' + String(task.id) + ' "' + String(task.subject) + '".']; + if (task.description && task.description !== task.subject) { + parts.push('\nDescription:\n' + String(task.description).slice(0, 500)); + } + parts.push( + '\n' + ${JSON.stringify(AGENT_BLOCK_OPEN)}, + 'Update task status using:', + 'node "$HOME/.claude/tools/${TOOL_FILE_NAME}" --team ' + String(teamName) + ' task start ' + String(task.id), + 'node "$HOME/.claude/tools/${TOOL_FILE_NAME}" --team ' + String(teamName) + ' task complete ' + String(task.id), + ${JSON.stringify(AGENT_BLOCK_CLOSE)} + ); + sendInboxMessage(paths, teamName, { + to: effectiveOwner, + text: parts.join('\n'), + summary: 'Task #' + String(task.id) + ' assigned', + from, + }); + } + return; + } if (action === 'briefing') { taskBriefing(paths, teamName, args.flags); return; diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index d7db106b..281be120 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -340,6 +340,8 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string ``, `Task board operations — use teamctl.js via Bash:`, `- Create task: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task create --subject "..." --description "..." --owner "" --notify --from "${leadName}"`, + `- Assign/reassign owner: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-owner --notify --from "${leadName}"`, + `- Clear owner: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-owner clear`, `- Update status: node "$HOME/.claude/tools/teamctl.js" --team "${teamName}" task set-status `, ``, `Notification policy:`, diff --git a/src/renderer/components/team/kanban/KanbanBoard.tsx b/src/renderer/components/team/kanban/KanbanBoard.tsx index 4c12010e..b8c8d2cf 100644 --- a/src/renderer/components/team/kanban/KanbanBoard.tsx +++ b/src/renderer/components/team/kanban/KanbanBoard.tsx @@ -149,6 +149,7 @@ interface SortableKanbanTaskCardProps { columnId: KanbanColumnId; teamName: string; kanbanState: KanbanState; + compact?: boolean; taskMap: Map; members: ResolvedTeamMember[]; onRequestReview: (taskId: string) => void; @@ -169,6 +170,7 @@ const SortableKanbanTaskCard = ({ columnId, teamName, kanbanState, + compact, taskMap, members, onRequestReview, @@ -203,6 +205,7 @@ const SortableKanbanTaskCard = ({ columnId={columnId} kanbanTaskState={kanbanState.tasks[task.id]} hasReviewers={kanbanState.reviewers.length > 0} + compact={compact} taskMap={taskMap} members={members} onRequestReview={onRequestReview} @@ -303,7 +306,11 @@ export const KanbanBoard = ({ [onColumnOrderChange, groupedOrdered] ); - const renderCards = (columnId: KanbanColumnId, columnTasks: TeamTask[]): React.JSX.Element => { + const renderCards = ( + columnId: KanbanColumnId, + columnTasks: TeamTask[], + compact?: boolean + ): React.JSX.Element => { const addHandler = onAddTask && columnId === 'todo' ? () => onAddTask(false) @@ -343,6 +350,7 @@ export const KanbanBoard = ({ columnId={columnId} teamName={teamName} kanbanState={kanbanState} + compact={compact} taskMap={taskMap} members={members} onRequestReview={onRequestReview} @@ -373,6 +381,7 @@ export const KanbanBoard = ({ columnId={columnId} kanbanTaskState={kanbanState.tasks[task.id]} hasReviewers={kanbanState.reviewers.length > 0} + compact={compact} taskMap={taskMap} members={members} onRequestReview={onRequestReview} @@ -497,7 +506,7 @@ export const KanbanBoard = ({ headerBg={accent.headerBg} bodyBg={accent.bodyBg} > - {renderCards(column.id, columnTasks)} + {renderCards(column.id, columnTasks, true)} ); diff --git a/src/renderer/components/team/kanban/KanbanColumn.tsx b/src/renderer/components/team/kanban/KanbanColumn.tsx index 61a9bf03..99627d58 100644 --- a/src/renderer/components/team/kanban/KanbanColumn.tsx +++ b/src/renderer/components/team/kanban/KanbanColumn.tsx @@ -38,7 +38,7 @@ export const KanbanColumn = ({ {count} -
{children}
+
{children}
); }; diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx index df07aac2..a778efc5 100644 --- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx +++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx @@ -27,6 +27,7 @@ interface KanbanTaskCardProps { columnId: KanbanColumnId; kanbanTaskState?: KanbanTaskState; hasReviewers: boolean; + compact?: boolean; taskMap: Map; members: ResolvedTeamMember[]; onRequestReview: (taskId: string) => void; @@ -134,6 +135,7 @@ export const KanbanTaskCard = ({ columnId, kanbanTaskState: _kanbanTaskState, hasReviewers, + compact, taskMap, members, onRequestReview, @@ -186,26 +188,35 @@ export const KanbanTaskCard = ({ } }} > -
- - #{task.id} - - {task.owner ? : null} - {task.needsClarification ? ( - - - {task.needsClarification === 'user' ? 'Awaiting user' : 'Awaiting lead'} - - ) : null} -
- {task.subject} -
+
+
+ + #{task.id} + + {task.owner ? : null} + {task.needsClarification ? ( + + + {task.needsClarification === 'user' ? 'Awaiting user' : 'Awaiting lead'} + + ) : null} + {!compact && ( +
+ {task.subject} +
+ )} +
+ {compact && ( +
+ {task.subject} +
+ )}
{hasBlockedBy ? ( @@ -332,12 +343,14 @@ export const KanbanTaskCard = ({