fix(chat): show context text before a pending question
A turn blocked on a question never reaches finish 'stop', so in sorted render mode the model's text was classified as justification and the inline-text deferral rule hid it inside the collapsible Activity group until the turn completed — with a pending question that never happens, leaving the context produced before the question invisible (OpenCode shows it inline). Keep text inline for messages that contain a question tool part: exclude them from justification classification and from the sorted-mode text deferral. Refs OPE-199
This commit is contained in:
@@ -97,6 +97,16 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit
|
|||||||
input.assistantMessages.forEach((message) => {
|
input.assistantMessages.forEach((message) => {
|
||||||
const finish = getMessageFinish(message);
|
const finish = getMessageFinish(message);
|
||||||
const messageHasTool = message.parts.some((part) => part.type === 'tool');
|
const messageHasTool = message.parts.some((part) => part.type === 'tool');
|
||||||
|
// A turn blocked on a question never reaches finish === 'stop' (the
|
||||||
|
// user must answer first). Treating the text the model produced
|
||||||
|
// before the question as 'justification' would bury it inside the
|
||||||
|
// collapsible Activity group — the context stays invisible until the
|
||||||
|
// turn completes (OPE-199). Keep it inline like OpenCode.
|
||||||
|
const messageHasQuestion = message.parts.some((part) => (
|
||||||
|
part.type === 'tool'
|
||||||
|
&& typeof part.tool === 'string'
|
||||||
|
&& part.tool.toLowerCase() === 'question'
|
||||||
|
));
|
||||||
const messageIsCompactionSummary = isCompactionSummaryMessage(message);
|
const messageIsCompactionSummary = isCompactionSummaryMessage(message);
|
||||||
|
|
||||||
message.parts.forEach((part, partIndex) => {
|
message.parts.forEach((part, partIndex) => {
|
||||||
@@ -137,6 +147,7 @@ export const projectTurnActivity = (input: ProjectActivityInput): ProjectActivit
|
|||||||
input.showTextJustificationActivity
|
input.showTextJustificationActivity
|
||||||
&& part.type === 'text'
|
&& part.type === 'text'
|
||||||
&& text
|
&& text
|
||||||
|
&& !messageHasQuestion
|
||||||
&& (
|
&& (
|
||||||
messageIsCompactionSummary
|
messageIsCompactionSummary
|
||||||
|| (
|
|| (
|
||||||
|
|||||||
@@ -221,4 +221,34 @@ describe('projectTurnRecords', () => {
|
|||||||
const finalActivity = turn?.activityParts.find((activity) => activity.messageId === 'a2');
|
const finalActivity = turn?.activityParts.find((activity) => activity.messageId === 'a2');
|
||||||
expect(finalActivity).toBe(undefined);
|
expect(finalActivity).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('keeps text inline (not justification) when a message is blocked on a pending question', () => {
|
||||||
|
const user = createMessageEntry({ id: 'u1', role: 'user', createdAt: 1 });
|
||||||
|
user.parts = [{ id: 'p1', type: 'text', text: 'prompt' } as Part];
|
||||||
|
const assistant = createMessageEntry({ id: 'a1', role: 'assistant', parentID: 'u1', createdAt: 2 });
|
||||||
|
// The turn is blocked waiting for the user's answer: no finish and a
|
||||||
|
// pending question tool part, with context text before the question.
|
||||||
|
assistant.parts = [
|
||||||
|
{ id: 'ap1', type: 'text', text: 'context before the question' } as Part,
|
||||||
|
{
|
||||||
|
id: 'ap2',
|
||||||
|
type: 'tool',
|
||||||
|
callID: 'c1',
|
||||||
|
tool: 'question',
|
||||||
|
state: { status: 'pending' },
|
||||||
|
} as Part,
|
||||||
|
];
|
||||||
|
|
||||||
|
const projection = projectTurnRecords([user, assistant], {
|
||||||
|
showTextJustificationActivity: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const turn = projection.turns[0];
|
||||||
|
expect(turn).toBeDefined();
|
||||||
|
const textActivity = turn?.activityParts.find((activity) => activity.partIndex === 0);
|
||||||
|
expect(textActivity?.kind).not.toBe('justification');
|
||||||
|
// The question tool itself still participates in the activity group.
|
||||||
|
const questionActivity = turn?.activityParts.find((activity) => activity.partIndex === 1);
|
||||||
|
expect(questionActivity?.kind).toBe('tool');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1690,7 +1690,16 @@ const AssistantMessageBody = React.memo(({
|
|||||||
&& hasAnchoredActivitySegments
|
&& hasAnchoredActivitySegments
|
||||||
&& Boolean(toggleActivityGroup);
|
&& Boolean(toggleActivityGroup);
|
||||||
|
|
||||||
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish;
|
// A message that asked a question is blocked until the user answers — it
|
||||||
|
// never reaches finish === 'stop', so the normal "defer text until final
|
||||||
|
// output" rule would hide the context the model produced before the
|
||||||
|
// question indefinitely (OPE-199). Render such messages' text inline,
|
||||||
|
// matching OpenCode's display.
|
||||||
|
const hasQuestionTool = React.useMemo(() => {
|
||||||
|
return toolParts.some((toolPart) => toolPart.tool === 'question');
|
||||||
|
}, [toolParts]);
|
||||||
|
|
||||||
|
const shouldDeferSortedInlineText = isSortedRenderMode && !hasStopFinish && !hasQuestionTool;
|
||||||
const showErrorMessage = Boolean(errorMessage);
|
const showErrorMessage = Boolean(errorMessage);
|
||||||
const errorIconName = errorVariant === 'info' ? 'information' : 'error-warning';
|
const errorIconName = errorVariant === 'info' ? 'information' : 'error-warning';
|
||||||
const shouldShowMessageActions = hasCopyableText;
|
const shouldShowMessageActions = hasCopyableText;
|
||||||
|
|||||||
Reference in New Issue
Block a user