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
30 changes: 15 additions & 15 deletions src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,23 @@ describeWithJsdom('ReviewTeamPage', () => {
vi.clearAllMocks();
});

async function waitForText(text: string, maxTicks = 20) {
for (let i = 0; i < maxTicks; i++) {
await act(async () => {
await Promise.resolve();
});
if (container.textContent?.includes(text)) return;
}
throw new Error(`waitForText: "${text}" not found after ${maxTicks} ticks`);
}

it('loads review team data only once on initial render', async () => {
const { default: ReviewTeamPage } = await import('./ReviewTeamPage');

await act(async () => {
root.render(<ReviewTeamPage />);
});
await act(async () => {
await Promise.resolve();
});
await waitForText('Team Overview');

expect(loadDefaultReviewTeam).toHaveBeenCalledTimes(1);
});
Expand All @@ -177,9 +185,7 @@ describeWithJsdom('ReviewTeamPage', () => {
await act(async () => {
root.render(<ReviewTeamPage />);
});
await act(async () => {
await Promise.resolve();
});
await waitForText('Team Overview');

expect(container.textContent).toContain('Team Overview');
expect(container.textContent).toContain('Current Policy');
Expand All @@ -200,9 +206,7 @@ describeWithJsdom('ReviewTeamPage', () => {
await act(async () => {
root.render(<ReviewTeamPage />);
});
await act(async () => {
await Promise.resolve();
});
await waitForText('Team Overview');

const settingsButton = Array.from(container.querySelectorAll('button'))
.find((button) => button.textContent?.includes('Review settings'));
Expand All @@ -227,9 +231,7 @@ describeWithJsdom('ReviewTeamPage', () => {
await act(async () => {
root.render(<ReviewTeamPage />);
});
await act(async () => {
await Promise.resolve();
});
await waitForText('Team Overview');

const policyPanel = container.querySelector<HTMLButtonElement>('.review-team-page__policy-panel');
expect(policyPanel).toBeTruthy();
Expand Down Expand Up @@ -287,9 +289,7 @@ describeWithJsdom('ReviewTeamPage', () => {
await act(async () => {
root.render(<ReviewTeamPage />);
});
await act(async () => {
await Promise.resolve();
});
await waitForText('Logic');

const memberButton = Array.from(container.querySelectorAll('button'))
.find((button) => button.textContent?.includes('Logic'));
Expand Down
16 changes: 8 additions & 8 deletions src/web-ui/src/flow_chat/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import React, { useRef, useCallback, useEffect, useReducer, useState, useMemo }
import { Trans, useTranslation } from 'react-i18next';
import { ArrowUp, Image, Maximize2, Minimize2, RotateCcw, Plus, X, Sparkles, Loader2, ChevronRight, Files, MessageSquarePlus } from 'lucide-react';
import { ContextDropZone, useContextStore } from '../../shared/context-system';
import { useActiveSessionState } from '../hooks/useActiveSessionState';
import { useActiveSessionState } from '@/flow_chat/hooks';
import { RichTextInput, type MentionState } from './RichTextInput';
import { FileMentionPicker } from './FileMentionPicker';
import { globalEventBus } from '../../infrastructure/event-bus';
import { globalEventBus } from '@/infrastructure';
import {
useSessionDerivedState,
useSessionStateMachine,
Expand All @@ -20,7 +20,7 @@ import { SessionExecutionEvent } from '../state-machine/types';
import { ModelSelector } from './ModelSelector';
import { FlowChatStore } from '../store/FlowChatStore';
import type { FlowChatState } from '../types/flow-chat';
import type { FileContext, DirectoryContext, ImageContext } from '../../shared/types/context';
import type { FileContext, DirectoryContext, ImageContext } from '@/types/context.ts';
import { SmartRecommendations } from './smart-recommendations';
import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext';
import { WorkspaceKind } from '@/shared/types';
Expand All @@ -33,10 +33,11 @@ import { useMessageSender } from '../hooks/useMessageSender';
import { useChatInputState } from '../store/chatInputStateStore';
import { useInputHistoryStore } from '../store/inputHistoryStore';
import { startBtwThread } from '../services/BtwThreadService';
import { FlowChatManager } from '../services/FlowChatManager';
import { FlowChatManager } from '@/flow_chat';
import {
DEEP_REVIEW_SLASH_COMMAND,
buildDeepReviewPromptFromSlashCommand,
getDeepReviewLaunchErrorMessage,
isDeepReviewSlashCommand,
launchDeepReviewSession,
} from '../services/DeepReviewService';
Expand Down Expand Up @@ -1137,8 +1138,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
if (text.startsWith('/')) {
const afterSlash = text.slice(1);
const hasWhitespace = /\s/.test(afterSlash);
const firstToken = afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? '';
const query = firstToken;
const query = afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? '';
const matchedMcpPrompt = resolveTypedMcpPromptCommand(text);

// While the main session is running, expose a single quick action (/btw) via the same picker UX.
Expand Down Expand Up @@ -1453,7 +1453,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
dispatchInput({ type: 'ACTIVATE' });
dispatchInput({ type: 'SET_VALUE', payload: message });
notificationService.error(
error instanceof Error ? error.message : t('error.unknown'),
getDeepReviewLaunchErrorMessage(error, t, t('error.unknown')),
{
title: t('chatInput.deepreviewFailed', { defaultValue: 'Deep review failed' }),
duration: 5000,
Expand Down Expand Up @@ -2255,7 +2255,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({

const isCollapsedProcessing = !inputState.isActive && !!derivedState?.isProcessing;
const petReplacesStopChrome = agentCompanionEnabled && isCollapsedProcessing;
const petStopClickable = petReplacesStopChrome && !!derivedState?.canCancel;
const petStopClickable = petReplacesStopChrome && derivedState?.canCancel;
const collapsedPetSplitSend =
petReplacesStopChrome && derivedState?.sendButtonMode === 'split';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@
line-height: 1.35;
}

.deep-review-consent__token-estimate {
margin: 6px 0 0;
color: var(--color-text-muted);
font-size: 11px;
font-weight: 500;
font-variant-numeric: tabular-nums;
}

.deep-review-consent__footer {
display: flex;
align-items: center;
Expand Down
11 changes: 11 additions & 0 deletions src/web-ui/src/flow_chat/components/DeepReviewConsentDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import { Clock, Coins, ShieldCheck, X } from 'lucide-react';
import { estimateTokenConsumption, formatTokenCount } from '../utils/deepReviewExperience';
import { useTranslation } from 'react-i18next';
import { Button, Checkbox, Modal } from '@/component-library';
import { createLogger } from '@/shared/utils/logger';
Expand Down Expand Up @@ -106,6 +107,16 @@ export function useDeepReviewConsent(): DeepReviewConsentControls {
{t('deepReviewConsent.costLabel', { defaultValue: 'Higher token usage' })}
</span>
<p>{t('deepReviewConsent.cost')}</p>
<p className="deep-review-consent__token-estimate">
{(() => {
const est = estimateTokenConsumption(5);
return t('deepReviewConsent.estimatedTokens', {
min: formatTokenCount(est.min),
max: formatTokenCount(est.max),
defaultValue: 'Estimated: {{min}} - {{max}} tokens',
});
})()}
</p>
</div>
</div>
<div className="deep-review-consent__fact">
Expand Down
218 changes: 218 additions & 0 deletions src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,224 @@
flex-shrink: 0;
}

/* Progress tracking */
&__progress {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 10px;
border-radius: 6px;
background: var(--element-bg-subtle);
font-size: 12px;
}

&__progress-text {
color: var(--color-text-secondary);
font-weight: 500;
}

&__elapsed {
margin-left: auto;
color: var(--color-text-muted);
font-variant-numeric: tabular-nums;
}

/* Partial results */
&__partial-summary {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 8px;
background: var(--element-bg-subtle);
font-size: 12px;
}

&__partial-count {
color: var(--color-text-secondary);
}

&__partial-link {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: auto;
color: var(--color-accent-500, #60a5fa);
background: none;
border: none;
cursor: pointer;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
transition: background 0.2s;

&:hover {
background: color-mix(in srgb, var(--color-accent-500, #60a5fa) 10%, transparent);
}
}

&__partial-detail {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
background: var(--color-bg-primary);
border: 1px solid var(--border-base);
}

&__partial-section {
font-size: 12px;
color: var(--color-text-secondary);
}

&__partial-section-title {
font-weight: 500;
}

/* Error attribution */
&__attribution {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;

&--warning {
background: color-mix(in srgb, var(--color-warning, #f59e0b) 8%, var(--color-bg-secondary));
border: 1px solid color-mix(in srgb, var(--color-warning, #f59e0b) 18%, transparent);
}

&--error {
background: color-mix(in srgb, var(--color-error, #ef4444) 8%, var(--color-bg-secondary));
border: 1px solid color-mix(in srgb, var(--color-error, #ef4444) 18%, transparent);
}
}

&__attribution-message {
color: var(--color-text-secondary);
line-height: 1.5;
}

&__attribution-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}

/* Recovery plan */
&__recovery-plan {
display: flex;
flex-direction: column;
gap: 6px;
}

&__recovery-plan-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-radius: 6px;
background: transparent;
border: 1px solid transparent;
color: var(--color-text-muted);
font-size: 12px;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, color 0.2s;
width: fit-content;

&:hover {
background: var(--element-bg-subtle);
border-color: var(--border-base);
color: var(--color-text-primary);
}
}

&__recovery-plan-detail {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 12px;
border-radius: 8px;
background: var(--color-bg-primary);
border: 1px solid var(--border-base);
}

&__recovery-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-text-secondary);
}

&__recovery-icon {
&--preserve {
color: var(--color-success, #22c55e);
}

&--rerun {
color: var(--color-accent-500, #60a5fa);
}

&--skip {
color: var(--color-text-muted);
}
}

/* Degradation options */
&__degradation {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
background: var(--color-bg-primary);
border: 1px solid var(--border-base);
}

&__degradation-title {
font-size: 12px;
font-weight: 600;
color: var(--color-text-primary);
}

&__degradation-option {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px 10px;
border-radius: 6px;
background: var(--element-bg-subtle);
border: 1px solid transparent;
cursor: pointer;
text-align: left;
transition: background 0.2s, border-color 0.2s;

&:hover:not(:disabled) {
background: var(--element-bg-base);
border-color: var(--border-base);
}

&:disabled {
opacity: 0.45;
cursor: not-allowed;
}
}

&__degradation-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-primary);
}

&__degradation-desc {
font-size: 11px;
color: var(--color-text-muted);
}

/* Animations */
@keyframes deep-review-action-bar-spin {
from {
Expand Down
Loading
Loading