diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index b07f4318..950bf628 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -157,6 +157,10 @@ interface ProvisioningRun { * Flushed to liveLeadProcessMessages on result.success. */ directReplyParts: string[]; + /** Whether we already emitted live lead text during the current turn (before result). */ + leadTextPushedInCurrentTurn: boolean; + /** Throttle timestamp for emitting inbox refresh events for lead text. */ + lastLeadTextEmitMs: number; /** * When set, the current stdin-injected turn is an internal "forward user DM to teammate" * request triggered by the UI. We suppress any lead→user echo for that turn. @@ -1166,6 +1170,8 @@ export class TeamProvisioningService { } private static readonly CONTEXT_EMIT_THROTTLE_MS = 2000; + private static readonly LEAD_TEXT_EMIT_THROTTLE_MS = 2000; + private static readonly LEAD_TEXT_MIN_LENGTH = 30; private emitLeadContextUsage(run: ProvisioningRun): void { if (!run.leadContextUsage || !run.provisioningComplete) return; @@ -1694,6 +1700,8 @@ export class TeamProvisioningService { fsPhase: 'waiting_config', leadRelayCapture: null, directReplyParts: [], + leadTextPushedInCurrentTurn: false, + lastLeadTextEmitMs: 0, silentUserDmForward: null, silentUserDmForwardClearHandle: null, provisioningOutputParts: [], @@ -1992,6 +2000,8 @@ export class TeamProvisioningService { fsPhase: 'waiting_members', leadRelayCapture: null, directReplyParts: [], + leadTextPushedInCurrentTurn: false, + lastLeadTextEmitMs: 0, silentUserDmForward: null, silentUserDmForwardClearHandle: null, provisioningOutputParts: [], @@ -2798,6 +2808,39 @@ export class TeamProvisioningService { return; } logger.debug(`[${run.teamName}] assistant: ${text.slice(0, 200)}`); + // After provisioning, surface lead assistant output in Messages immediately. + // Lead session JSONL changes are not watched, so without an explicit trigger + // the Messages tab may lag behind Claude Logs until another team-change event. + if (run.provisioningComplete && !run.leadRelayCapture && !run.silentUserDmForward) { + const cleanText = stripAgentBlocks(text).trim(); + if (cleanText.length >= TeamProvisioningService.LEAD_TEXT_MIN_LENGTH) { + const leadName = + run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name || + 'team-lead'; + const leadMsg: InboxMessage = { + from: leadName, + to: 'user', + text: cleanText, + timestamp: nowIso(), + read: true, + summary: cleanText.length > 60 ? cleanText.slice(0, 57) + '...' : cleanText, + messageId: `lead-text-${run.runId}-${Date.now()}`, + source: 'lead_process', + }; + this.pushLiveLeadProcessMessage(run.teamName, leadMsg); + run.leadTextPushedInCurrentTurn = true; + + const now = Date.now(); + if (now - run.lastLeadTextEmitMs >= TeamProvisioningService.LEAD_TEXT_EMIT_THROTTLE_MS) { + run.lastLeadTextEmitMs = now; + this.teamChangeEmitter?.({ + type: 'inbox', + teamName: run.teamName, + detail: 'lead-text', + }); + } + } + } // During provisioning (before provisioningComplete), accumulate for live UI preview. // Emission is handled by the throttled emitLogsProgress() in the stdout data handler. if (!run.provisioningComplete) { @@ -2965,59 +3008,63 @@ export class TeamProvisioningService { // Flush accumulated assistant reply from direct user→lead message const rawReply = run.directReplyParts.join('').trim(); run.directReplyParts = []; - const leadName = - run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name || - 'team-lead'; - // Strip agent-only blocks — lead may include coordination content not meant for the user - const replyText = stripAgentBlocks(rawReply); - if (replyText.length > 0) { - const replyMsg: InboxMessage = { - from: leadName, - to: 'user', - text: replyText, - timestamp: nowIso(), - read: true, - summary: replyText.length > 60 ? replyText.slice(0, 57) + '...' : replyText, - messageId: `lead-direct-${run.runId}-${Date.now()}`, - source: 'lead_process', - }; - this.pushLiveLeadProcessMessage(run.teamName, replyMsg); - // Persist to disk so replies survive app restart - void this.sentMessagesStore - .appendMessage(run.teamName, replyMsg) - .catch((e: unknown) => - logger.warn(`[${run.teamName}] sentMessagesStore persist failed: ${e}`) - ); - this.teamChangeEmitter?.({ - type: 'inbox', - teamName: run.teamName, - detail: 'lead-direct-reply', - }); - } else if (rawReply.length > 0) { - // Lead responded but only with agent-only content — send generic acknowledgment - const fallbackMsg: InboxMessage = { - from: leadName, - to: 'user', - text: '(Message received and processed)', - timestamp: nowIso(), - read: true, - summary: 'Message processed', - messageId: `lead-direct-${run.runId}-${Date.now()}`, - source: 'lead_process', - }; - this.pushLiveLeadProcessMessage(run.teamName, fallbackMsg); - void this.sentMessagesStore - .appendMessage(run.teamName, fallbackMsg) - .catch((e: unknown) => - logger.warn(`[${run.teamName}] sentMessagesStore persist failed: ${e}`) - ); - this.teamChangeEmitter?.({ - type: 'inbox', - teamName: run.teamName, - detail: 'lead-direct-reply', - }); + if (!run.leadTextPushedInCurrentTurn) { + const leadName = + run.request.members.find((m) => m.role?.toLowerCase().includes('lead'))?.name || + 'team-lead'; + // Strip agent-only blocks — lead may include coordination content not meant for the user + const replyText = stripAgentBlocks(rawReply); + if (replyText.length > 0) { + const replyMsg: InboxMessage = { + from: leadName, + to: 'user', + text: replyText, + timestamp: nowIso(), + read: true, + summary: replyText.length > 60 ? replyText.slice(0, 57) + '...' : replyText, + messageId: `lead-direct-${run.runId}-${Date.now()}`, + source: 'lead_process', + }; + this.pushLiveLeadProcessMessage(run.teamName, replyMsg); + // Persist to disk so replies survive app restart + void this.sentMessagesStore + .appendMessage(run.teamName, replyMsg) + .catch((e: unknown) => + logger.warn(`[${run.teamName}] sentMessagesStore persist failed: ${e}`) + ); + this.teamChangeEmitter?.({ + type: 'inbox', + teamName: run.teamName, + detail: 'lead-direct-reply', + }); + } else if (rawReply.length > 0) { + // Lead responded but only with agent-only content — send generic acknowledgment + const fallbackMsg: InboxMessage = { + from: leadName, + to: 'user', + text: '(Message received and processed)', + timestamp: nowIso(), + read: true, + summary: 'Message processed', + messageId: `lead-direct-${run.runId}-${Date.now()}`, + source: 'lead_process', + }; + this.pushLiveLeadProcessMessage(run.teamName, fallbackMsg); + void this.sentMessagesStore + .appendMessage(run.teamName, fallbackMsg) + .catch((e: unknown) => + logger.warn(`[${run.teamName}] sentMessagesStore persist failed: ${e}`) + ); + this.teamChangeEmitter?.({ + type: 'inbox', + teamName: run.teamName, + detail: 'lead-direct-reply', + }); + } } } + // Turn boundary: reset per-turn lead text tracking. + run.leadTextPushedInCurrentTurn = false; // Clear silent relay flag after any successful turn. run.silentUserDmForward = null; if (run.silentUserDmForwardClearHandle) { @@ -3034,6 +3081,8 @@ export class TeamProvisioningService { if (run.leadRelayCapture) { run.leadRelayCapture.rejectOnce(errorMsg); } + // Turn boundary: reset per-turn lead text tracking. + run.leadTextPushedInCurrentTurn = false; // Clear silent relay flag after any errored turn. run.silentUserDmForward = null; if (run.silentUserDmForwardClearHandle) { diff --git a/src/renderer/components/chat/DisplayItemList.tsx b/src/renderer/components/chat/DisplayItemList.tsx index 59dd25ae..d6f90ad3 100644 --- a/src/renderer/components/chat/DisplayItemList.tsx +++ b/src/renderer/components/chat/DisplayItemList.tsx @@ -29,6 +29,8 @@ interface DisplayItemListProps { onItemClick: (itemId: string) => void; expandedItemIds: Set; aiGroupId: string; + /** Render order for display items (visual only). */ + order?: 'chronological' | 'newest-first'; /** Optional local search query override for markdown highlighting */ searchQueryOverride?: string; /** Tool use ID to highlight for error deep linking */ @@ -68,6 +70,7 @@ export const DisplayItemList = ({ onItemClick, expandedItemIds, aiGroupId, + order = 'chronological', searchQueryOverride, highlightToolUseId, highlightColor, @@ -99,7 +102,13 @@ export const DisplayItemList = ({ } return ( -
+
{items.map((item, index) => { let itemKey = ''; let element: React.ReactNode = null; diff --git a/src/renderer/components/team/TaskTooltip.tsx b/src/renderer/components/team/TaskTooltip.tsx index 7fcc31fc..f260dccc 100644 --- a/src/renderer/components/team/TaskTooltip.tsx +++ b/src/renderer/components/team/TaskTooltip.tsx @@ -111,7 +111,7 @@ export const TaskTooltip = ({ color={colorMap.get(task.owner)} /> ) : ( - Не назначено + Unassigned )}
diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 64ab3f9c..e9efd55e 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -1381,6 +1381,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele title="CLI Processes" icon={} badge={data.processes.filter((p) => !p.stoppedAt).length} + headerExtra={ + data.processes.some((p) => !p.stoppedAt) ? ( + + + + + ) : null + } defaultOpen > diff --git a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx index 405cfdb7..f36254b3 100644 --- a/src/renderer/components/team/dialogs/TaskDetailDialog.tsx +++ b/src/renderer/components/team/dialogs/TaskDetailDialog.tsx @@ -357,7 +357,7 @@ export const TaskDetailDialog = ({ size="md" /> ) : ( - Не назначено + Unassigned )}
{currentTask.createdBy ? ( diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx index 29414aca..e86968a7 100644 --- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx +++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx @@ -270,9 +270,7 @@ export const KanbanTaskCard = ({
{task.owner ? ( - ) : ( - Не назначено - )} + ) : null} {!compact && }
{task.needsClarification ? ( diff --git a/src/renderer/components/team/kanban/TrashDialog.tsx b/src/renderer/components/team/kanban/TrashDialog.tsx index 97cd1d53..fc87384a 100644 --- a/src/renderer/components/team/kanban/TrashDialog.tsx +++ b/src/renderer/components/team/kanban/TrashDialog.tsx @@ -66,7 +66,7 @@ export const TrashDialog = ({ {task.id} {task.subject} - {task.owner ?? 'Не назначено'} + {task.owner ?? 'Unassigned'} {task.deletedAt diff --git a/src/renderer/components/team/members/MemberExecutionLog.tsx b/src/renderer/components/team/members/MemberExecutionLog.tsx index 061ca544..7073553f 100644 --- a/src/renderer/components/team/members/MemberExecutionLog.tsx +++ b/src/renderer/components/team/members/MemberExecutionLog.tsx @@ -214,6 +214,7 @@ const AIExecutionGroup = ({
{ {task.id} {task.subject} - {task.owner ?? 'Не назначено'} + {task.owner ?? 'Unassigned'} {task.kanbanColumn && task.kanbanColumn in KANBAN_COLUMN_DISPLAY ? KANBAN_COLUMN_DISPLAY[task.kanbanColumn].label