From db08b0ae9ebe873caf01318fe96d218e374df0c1 Mon Sep 17 00:00:00 2001 From: iliya Date: Tue, 10 Mar 2026 13:34:10 +0200 Subject: [PATCH] feat: enhance team provisioning instructions and context handling - Added clarification on handling review requests in TeamProvisioningService to prevent redundant notifications. - Introduced a new utility function to compute remaining context in contextMath, improving context management. - Updated ChatHistory component to display remaining context urgency and percentage. - Enhanced UserChatGroup with improved expand/collapse functionality and sticky "Show less" button. - Updated tests to validate new context handling and messaging instructions. --- .../services/team/TeamProvisioningService.ts | 6 +++ src/renderer/components/chat/ChatHistory.tsx | 29 +++++++++++++- .../components/chat/UserChatGroup.tsx | 38 +++++++++++++++---- .../components/team/activity/ActivityItem.tsx | 2 +- .../team/activity/AnimatedHeightReveal.tsx | 2 +- src/renderer/utils/contextMath.ts | 24 ++++++++++++ src/shared/utils/modelParser.ts | 3 ++ .../TeamProvisioningServicePrompts.test.ts | 6 +++ .../team/TeamProvisioningServiceRelay.test.ts | 3 ++ 9 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index b1808ad2..8c8b704a 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -508,6 +508,7 @@ function buildTeamCtlOpsInstructions(teamName: string, leadName: string): string ``, `Notification policy:`, `- Task assignment notifications are handled by the board runtime, so do NOT send a separate SendMessage for the same assignment unless you have extra context that is not already on the task.`, + `- Review requests are also handled by the board runtime: review_request already notifies the reviewer, so do NOT send a second manual SendMessage for the same review request unless you are adding materially new context that is not already on the task.`, ``, `Clarification handling (CRITICAL — MANDATORY for correct task board state):`, `- When a teammate needs clarification (needsClarification: "lead"), reply via task comment (preferred — auto-clears the flag and wakes the owner) or SendMessage.`, @@ -2706,6 +2707,8 @@ export class TeamProvisioningService { [ `Use the SendMessage tool with recipient="${memberName}".`, `Forward each inbox item below as a teammate message, preserving task IDs and critical instructions.`, + `If an inbox item is marked Source: system_notification, treat it as an automated runtime notification.`, + `Forward that automated notification exactly once; do NOT send an additional paraphrased/manual follow-up for the same assignment/review/comment in this relay turn unless you truly need extra non-redundant context.`, `Do NOT send any message to recipient "user" for this relay turn.`, `Do NOT add extra narration outside the SendMessage calls.`, ].join('\n') @@ -2733,6 +2736,9 @@ export class TeamProvisioningService { `${idx + 1}) From: ${m.from || 'unknown'}`, ` Timestamp: ${m.timestamp}`, ...(summaryLine ? [` ${summaryLine}`] : []), + ...(typeof m.source === 'string' && m.source.trim() + ? [` Source: ${m.source.trim()}`] + : []), ...replyInstructions, ` Text:`, ...m.text.split('\n').map((line) => ` ${line}`), diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 734f53b1..615b5098 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -16,7 +16,11 @@ const SCROLL_THRESHOLD = 300; /** Must match the `w-80` (320px) context panel width used in the layout below. */ const CONTEXT_PANEL_WIDTH_PX = 320; -import { formatPercentOfTotal, sumContextInjectionTokens } from '@renderer/utils/contextMath'; +import { + computeRemainingContext, + formatPercentOfTotal, + sumContextInjectionTokens, +} from '@renderer/utils/contextMath'; import { ChatHistoryEmptyState } from './ChatHistoryEmptyState'; import { ChatHistoryItem } from './ChatHistoryItem'; @@ -197,6 +201,11 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { return formatPercentOfTotal(visibleTokens, lastAiGroupTotalTokens); }, [allContextInjections, lastAiGroupTotalTokens]); + const remainingContext = useMemo( + () => computeRemainingContext(lastAiGroupTotalTokens), + [lastAiGroupTotalTokens] + ); + // State for navigation highlight (blue, used for Turn navigation from CLAUDE.md panel) const [isNavigationHighlight, setIsNavigationHighlight] = useState(false); const navigationHighlightTimerRef = useRef | null>(null); @@ -835,7 +844,23 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { : 'var(--color-text-secondary)', }} > - {visibleContextPercentLabel ?? `Context (${allContextInjections.length})`} + {visibleContextPercentLabel ? ( + <> + {visibleContextPercentLabel} + {remainingContext && remainingContext.urgency !== 'normal' && ( + + {' '} + ({remainingContext.remainingPct.toFixed(0)}% left) + + )} + + ) : ( + `Context (${allContextInjections.length})` + )} )} diff --git a/src/renderer/components/chat/UserChatGroup.tsx b/src/renderer/components/chat/UserChatGroup.tsx index ef14324a..3d3b34eb 100644 --- a/src/renderer/components/chat/UserChatGroup.tsx +++ b/src/renderer/components/chat/UserChatGroup.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'; import { api } from '@renderer/api'; @@ -13,7 +13,7 @@ import { linkifyMentionsInMarkdown } from '@renderer/utils/mentionLinkify'; import { stripAgentBlocks } from '@shared/constants/agentBlocks'; import { createLogger } from '@shared/utils/logger'; import { format } from 'date-fns'; -import { User } from 'lucide-react'; +import { ChevronDown, ChevronUp, User } from 'lucide-react'; import remarkGfm from 'remark-gfm'; import { useShallow } from 'zustand/react/shallow'; @@ -479,6 +479,12 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. // Combined expansion state: manual toggle or auto-expand for search const isExpanded = isManuallyExpanded || shouldAutoExpand; + const anchorRef = useRef(null); + const handleCollapse = useCallback(() => { + setIsManuallyExpanded(false); + anchorRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + }, []); + // Determine display text const baseDisplayText = isLongContent && !isExpanded ? stripped.slice(0, 500) + '...' : stripped; @@ -489,7 +495,7 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. ); return ( -
+
{/* Header - right aligned with improved hierarchy */}
@@ -524,18 +530,36 @@ const UserChatGroupInner = ({ userGroup }: Readonly): React. {displayText}
- {isLongContent && ( + {isLongContent && !isExpanded && ( )}
)} + {/* Sticky Show less — outside overflow-hidden bubble so sticky works */} + {stripped && isLongContent && isExpanded ? ( +
+ +
+ ) : null} + {/* Images indicator */} {hasImages && (
diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx index 37d31e87..56c5af74 100644 --- a/src/renderer/components/team/activity/ActivityItem.tsx +++ b/src/renderer/components/team/activity/ActivityItem.tsx @@ -425,7 +425,7 @@ export const ActivityItem = ({ return (
-
{children}
+
{children}
); }; diff --git a/src/renderer/utils/contextMath.ts b/src/renderer/utils/contextMath.ts index 83ec3351..f83c5474 100644 --- a/src/renderer/utils/contextMath.ts +++ b/src/renderer/utils/contextMath.ts @@ -1,3 +1,5 @@ +import { DEFAULT_CONTEXT_WINDOW } from '@shared/utils/modelParser'; + import type { ContextInjection } from '@renderer/types/contextInjection'; export function sumContextInjectionTokens(injections: readonly ContextInjection[]): number { @@ -25,3 +27,25 @@ export function formatPercentOfTotal( if (pct === null) return null; return `${pct.toFixed(1)}% of input`; } + +export type ContextUrgency = 'normal' | 'warning' | 'critical'; + +export interface RemainingContext { + remainingPct: number; + urgency: ContextUrgency; +} + +/** + * Compute how much context window remains before compaction. + * Returns null if input data is unavailable. + */ +export function computeRemainingContext( + totalInputTokens: number | undefined, + contextWindow: number = DEFAULT_CONTEXT_WINDOW +): RemainingContext | null { + if (totalInputTokens === undefined || totalInputTokens <= 0) return null; + const remainingPct = Math.max(((contextWindow - totalInputTokens) / contextWindow) * 100, 0); + const urgency: ContextUrgency = + remainingPct < 20 ? 'critical' : remainingPct < 40 ? 'warning' : 'normal'; + return { remainingPct, urgency }; +} diff --git a/src/shared/utils/modelParser.ts b/src/shared/utils/modelParser.ts index 8d9fbf37..9bfdb686 100644 --- a/src/shared/utils/modelParser.ts +++ b/src/shared/utils/modelParser.ts @@ -3,6 +3,9 @@ * Parses model identifiers into friendly display names and metadata. */ +/** Default context window size for Claude models (all current models use 200K) */ +export const DEFAULT_CONTEXT_WINDOW = 200_000; + /** Known model families with specific styling */ export type KnownModelFamily = 'sonnet' | 'opus' | 'haiku'; diff --git a/test/main/services/team/TeamProvisioningServicePrompts.test.ts b/test/main/services/team/TeamProvisioningServicePrompts.test.ts index cbd9318b..f4b93616 100644 --- a/test/main/services/team/TeamProvisioningServicePrompts.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrompts.test.ts @@ -112,6 +112,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(prompt).toContain('PROGRESS REPORTING (MANDATORY)'); expect(prompt).toContain('Never bulk-move many tasks at the end'); expect(prompt).toContain('Default to working ONE task at a time'); + expect(prompt).toContain( + 'review_request already notifies the reviewer, so do NOT send a second manual SendMessage for the same review request' + ); expect(prompt).toContain('task_start'); expect(prompt).toContain('task_complete'); expect(prompt).toContain('TURN ACTION MODE PROTOCOL (HIGHEST PRIORITY FOR EACH USER TURN):'); @@ -180,6 +183,9 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () => expect(prompt).toContain('Do NOT start the next task until the current task is completed'); expect(prompt).toContain('Do NOT delay this reconnect turn by reading internal config files'); expect(prompt).toContain('Treat it as a diagnostic cross-check, not as the first reconnect action.'); + expect(prompt).toContain( + 'review_request already notifies the reviewer, so do NOT send a second manual SendMessage for the same review request' + ); expect(prompt).toContain('task_start'); expect(prompt).toContain(`AGENT_BLOCK_OPEN is exactly: ${AGENT_BLOCK_OPEN}`); expect(prompt).toContain(`AGENT_BLOCK_CLOSE is exactly: ${AGENT_BLOCK_CLOSE}`); diff --git a/test/main/services/team/TeamProvisioningServiceRelay.test.ts b/test/main/services/team/TeamProvisioningServiceRelay.test.ts index 7c41fa9d..f665e642 100644 --- a/test/main/services/team/TeamProvisioningServiceRelay.test.ts +++ b/test/main/services/team/TeamProvisioningServiceRelay.test.ts @@ -421,6 +421,7 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { read: false, summary: 'Comment on #abcd1234', messageId: 'm-alice-1', + source: 'system_notification', }, ]); @@ -432,6 +433,8 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => { const payload = String(writeSpy.mock.calls[0]?.[0] ?? ''); expect(payload).toContain('"type":"user"'); expect(payload).toContain('recipient=\\"alice\\"'); + expect(payload).toContain('Source: system_notification'); + expect(payload).toContain('Forward that automated notification exactly once;'); expect(payload).toContain('Please retry with logging enabled.'); });