fix(ai): show answered ask_questions as a card in chat history (#23075)

## Context

Feedback on the `ask_questions` (Ask AI) tool:
- When answering a select prompt with a free-form message, the answer
wasn't surfaced as expected in the conversation history.
- The selected value also looked dropped once picked.

Root cause: answered `ask_questions` parts were caught by the
thinking-steps grouping and rendered as a generic collapsible "Ran
ask_questions" tool step (JSON output), so the dedicated renderer was
never reached.

## Changes

- **Render answered questions as a card**
(`AiChatQuestionStatusRenderer`): an "Answers" card that shows each full
question with the chosen option label(s) or the free-text answer beneath
it, instead of the faint inline `header: value` line.
- **Free-text keyboard navigation** (`AiChatQuestionCard`): pressing
Enter in the free-text area now advances to the next question, or
submits when on the last question (mirroring the option-select flow).
Shift+Enter still inserts a newline.

## Notes

- No schema/GraphQL changes; display + interaction only.
- Existing `thinkingStepsDisplayState` grouping test is unaffected (only
`web_search`/`create_task`/`code_interpreter` are used there).


<img width="421" height="301" alt="Screenshot 2026-07-20 at 17 48 04"
src="https://github.com/user-attachments/assets/3845055d-7061-40c0-b263-aa1c670329cd"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23075?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->


fixes
https://discord.com/channels/1130383047699738754/1526871110300209282
This commit is contained in:
Etienne
2026-07-20 18:53:51 +02:00
committed by GitHub
parent 05132d262b
commit 9abf76f5f2
4 changed files with 64 additions and 36 deletions
@@ -330,6 +330,13 @@ export const AiChatQuestionCard = ({
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
if (!isLastQuestion) {
setCurrentIndex(currentIndex + 1);
return;
}
handleSend();
}
};
@@ -22,26 +22,40 @@ const StyledContainer = styled.div`
}
`;
const StyledContent = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['0.5']};
min-width: 0;
`;
const StyledMessage = styled.span`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.md};
font-weight: ${themeCssVariables.font.weight.medium};
`;
const StyledAnswerLine = styled.span`
const StyledAnswersCard = styled.div`
background-color: ${themeCssVariables.background.transparent.lighter};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.sm};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[3]};
`;
const StyledAnswerBlock = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing['0.5']};
min-width: 0;
`;
const StyledAnswerQuestion = styled.span`
color: ${themeCssVariables.font.color.tertiary};
font-size: ${themeCssVariables.font.size.sm};
overflow-wrap: anywhere;
`;
const StyledAnswerValue = styled.span`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.sm};
overflow-wrap: anywhere;
`;
export const AiChatQuestionStatusRenderer = ({
@@ -79,34 +93,32 @@ export const AiChatQuestionStatusRenderer = ({
const answers = result?.answers ?? [];
return (
<StyledContainer>
<IconHelpCircle size={theme.icon.size.sm} />
<StyledContent>
<StyledMessage>{t`Questions answered`}</StyledMessage>
{questions.map((question, index) => {
const answer = answers.find(
(candidate) => candidate.questionIndex === index,
);
const selectedLabels = (answer?.selectedOptionIndices ?? [])
.map((optionIndex) => question.options[optionIndex]?.label)
.filter(isNonEmptyString);
const freeTextAnswer = answer?.freeText ?? '';
const value =
freeTextAnswer.length > 0
? freeTextAnswer
: selectedLabels.join(', ');
<StyledAnswersCard>
<StyledMessage>{t`Answers`}</StyledMessage>
{questions.map((question, index) => {
const answer = answers.find(
(candidate) => candidate.questionIndex === index,
);
const selectedLabels = (answer?.selectedOptionIndices ?? [])
.map((optionIndex) => question.options[optionIndex]?.label)
.filter(isNonEmptyString);
const freeTextAnswer = answer?.freeText ?? '';
const value =
freeTextAnswer.length > 0
? freeTextAnswer
: selectedLabels.join(', ');
if (value.length === 0) {
return null;
}
if (value.length === 0) {
return null;
}
return (
<StyledAnswerLine key={index}>
{question.header}: <StyledAnswerValue>{value}</StyledAnswerValue>
</StyledAnswerLine>
);
})}
</StyledContent>
</StyledContainer>
return (
<StyledAnswerBlock key={index}>
<StyledAnswerQuestion>{question.question}</StyledAnswerQuestion>
<StyledAnswerValue>{value}</StyledAnswerValue>
</StyledAnswerBlock>
);
})}
</StyledAnswersCard>
);
};
@@ -1,6 +1,7 @@
import { type ExtendedUIMessagePart } from 'twenty-shared/ai';
import { type AssistantMessageRenderItem } from '@/ai/utils/assistantMessageRenderItem';
import { isAskQuestionsToolPart } from '@/ai/utils/isAskQuestionsToolPart';
import { isThinkingStepPart } from '@/ai/utils/isThinkingStepPart';
import { type ThinkingStepPart } from '@/ai/utils/thinkingStepPart';
@@ -25,7 +26,7 @@ export const groupContiguousThinkingStepParts = (
continue;
}
if (isThinkingStepPart(part)) {
if (isThinkingStepPart(part) && !isAskQuestionsToolPart(part)) {
currentThinkingParts.push(part);
continue;
}
@@ -0,0 +1,8 @@
import { getToolName, isToolUIPart } from 'ai';
import {
ASK_QUESTIONS_TOOL_NAME,
type ExtendedUIMessagePart,
} from 'twenty-shared/ai';
export const isAskQuestionsToolPart = (part: ExtendedUIMessagePart): boolean =>
isToolUIPart(part) && getToolName(part) === ASK_QUESTIONS_TOOL_NAME;