feat(cli): overhaul thinking UI (#18725)
@@ -374,7 +374,7 @@ describe('Composer', () => {
|
||||
const uiState = createMockUIState({
|
||||
streamingState: StreamingState.Responding,
|
||||
thought: {
|
||||
subject: 'Detailed in-history thought',
|
||||
subject: 'Thinking about code',
|
||||
description: 'Full text is already in history',
|
||||
},
|
||||
});
|
||||
@@ -385,7 +385,7 @@ describe('Composer', () => {
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('LoadingIndicator: Thinking ...');
|
||||
expect(output).toContain('LoadingIndicator: Thinking...');
|
||||
});
|
||||
|
||||
it('hides shortcuts hint while loading', async () => {
|
||||
|
||||
@@ -239,7 +239,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
thoughtLabel={
|
||||
inlineThinkingMode === 'full' ? 'Thinking ...' : undefined
|
||||
inlineThinkingMode === 'full' ? 'Thinking...' : undefined
|
||||
}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
/>
|
||||
@@ -282,7 +282,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
: uiState.currentLoadingPhrase
|
||||
}
|
||||
thoughtLabel={
|
||||
inlineThinkingMode === 'full' ? 'Thinking ...' : undefined
|
||||
inlineThinkingMode === 'full' ? 'Thinking...' : undefined
|
||||
}
|
||||
elapsedTime={uiState.elapsedTime}
|
||||
/>
|
||||
@@ -390,7 +390,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
marginTop={
|
||||
(showApprovalIndicator ||
|
||||
uiState.shellModeActive) &&
|
||||
isNarrow
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
|
||||
@@ -290,6 +290,26 @@ describe('<HistoryItemDisplay />', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders "Thinking..." header when isFirstThinking is true', async () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
type: 'thinking',
|
||||
thought: { subject: 'Thinking', description: 'test' },
|
||||
};
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<HistoryItemDisplay {...baseItem} item={item} isFirstThinking={true} />,
|
||||
{
|
||||
settings: createMockSettings({
|
||||
merged: { ui: { inlineThinkingMode: 'full' } },
|
||||
}),
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toContain(' Thinking...');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
it('does not render thinking item when disabled', async () => {
|
||||
const item: HistoryItem = {
|
||||
...baseItem,
|
||||
|
||||
@@ -46,6 +46,8 @@ interface HistoryItemDisplayProps {
|
||||
commands?: readonly SlashCommand[];
|
||||
availableTerminalHeightGemini?: number;
|
||||
isExpandable?: boolean;
|
||||
isFirstThinking?: boolean;
|
||||
isFirstAfterThinking?: boolean;
|
||||
}
|
||||
|
||||
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
@@ -56,16 +58,30 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
||||
commands,
|
||||
availableTerminalHeightGemini,
|
||||
isExpandable,
|
||||
isFirstThinking = false,
|
||||
isFirstAfterThinking = false,
|
||||
}) => {
|
||||
const settings = useSettings();
|
||||
const inlineThinkingMode = getInlineThinkingMode(settings);
|
||||
const itemForDisplay = useMemo(() => escapeAnsiCtrlCodes(item), [item]);
|
||||
|
||||
const needsTopMarginAfterThinking =
|
||||
isFirstAfterThinking && inlineThinkingMode !== 'off';
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={itemForDisplay.id} width={terminalWidth}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
key={itemForDisplay.id}
|
||||
width={terminalWidth}
|
||||
marginTop={needsTopMarginAfterThinking ? 1 : 0}
|
||||
>
|
||||
{/* Render standard message types */}
|
||||
{itemForDisplay.type === 'thinking' && inlineThinkingMode !== 'off' && (
|
||||
<ThinkingMessage thought={itemForDisplay.thought} />
|
||||
<ThinkingMessage
|
||||
thought={itemForDisplay.thought}
|
||||
terminalWidth={terminalWidth}
|
||||
isFirstThinking={isFirstThinking}
|
||||
/>
|
||||
)}
|
||||
{itemForDisplay.type === 'hint' && (
|
||||
<HintMessage text={itemForDisplay.text} />
|
||||
|
||||
@@ -258,13 +258,32 @@ describe('<LoadingIndicator />', () => {
|
||||
const output = lastFrame();
|
||||
expect(output).toBeDefined();
|
||||
if (output) {
|
||||
expect(output).toContain('💬');
|
||||
// Should NOT contain "Thinking... " prefix because the subject already starts with "Thinking"
|
||||
expect(output).not.toContain('Thinking... Thinking');
|
||||
expect(output).toContain('Thinking about something...');
|
||||
expect(output).not.toContain('and other stuff.');
|
||||
}
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should prepend "Thinking... " if the subject does not start with "Thinking"', async () => {
|
||||
const props = {
|
||||
thought: {
|
||||
subject: 'Planning the response...',
|
||||
description: 'details',
|
||||
},
|
||||
elapsedTime: 5,
|
||||
};
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithContext(
|
||||
<LoadingIndicator {...props} />,
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Thinking... Planning the response...');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should prioritize thought.subject over currentLoadingPhrase', async () => {
|
||||
const props = {
|
||||
thought: {
|
||||
@@ -280,13 +299,13 @@ describe('<LoadingIndicator />', () => {
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('💬');
|
||||
expect(output).toContain('Thinking... ');
|
||||
expect(output).toContain('This should be displayed');
|
||||
expect(output).not.toContain('This should not be displayed');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not display thought icon for non-thought loading phrases', async () => {
|
||||
it('should not display thought indicator for non-thought loading phrases', async () => {
|
||||
const { lastFrame, unmount, waitUntilReady } = renderWithContext(
|
||||
<LoadingIndicator
|
||||
currentLoadingPhrase="some random tip..."
|
||||
@@ -295,7 +314,7 @@ describe('<LoadingIndicator />', () => {
|
||||
StreamingState.Responding,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).not.toContain('💬');
|
||||
expect(lastFrame()).not.toContain('Thinking... ');
|
||||
unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,11 @@ export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
|
||||
const hasThoughtIndicator =
|
||||
currentLoadingPhrase !== INTERACTIVE_SHELL_WAITING_PHRASE &&
|
||||
Boolean(thought?.subject?.trim());
|
||||
const thinkingIndicator = hasThoughtIndicator ? '💬 ' : '';
|
||||
// Avoid "Thinking... Thinking..." duplication if primaryText already starts with "Thinking"
|
||||
const thinkingIndicator =
|
||||
hasThoughtIndicator && !primaryText?.startsWith('Thinking')
|
||||
? 'Thinking... '
|
||||
: '';
|
||||
|
||||
const cancelAndTimerContent =
|
||||
showCancelAndTimer &&
|
||||
|
||||
@@ -22,17 +22,19 @@ import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import { type IndividualToolCallDisplay } from '../types.js';
|
||||
|
||||
// Mock dependencies
|
||||
const mockUseSettings = vi.fn().mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
inlineThinkingMode: 'off',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||
const actual = await vi.importActual('../contexts/SettingsContext.js');
|
||||
return {
|
||||
...actual,
|
||||
useSettings: () => ({
|
||||
merged: {
|
||||
ui: {
|
||||
inlineThinkingMode: 'off',
|
||||
},
|
||||
},
|
||||
}),
|
||||
useSettings: () => mockUseSettings(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -333,6 +335,13 @@ describe('MainContent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(false);
|
||||
mockUseSettings.mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
inlineThinkingMode: 'off',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -570,6 +579,64 @@ describe('MainContent', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders multiple thinking messages sequentially correctly', async () => {
|
||||
mockUseSettings.mockReturnValue({
|
||||
merged: {
|
||||
ui: {
|
||||
inlineThinkingMode: 'expanded',
|
||||
},
|
||||
},
|
||||
});
|
||||
vi.mocked(useAlternateBuffer).mockReturnValue(true);
|
||||
|
||||
const uiState = {
|
||||
...defaultMockUiState,
|
||||
history: [
|
||||
{ id: 0, type: 'user' as const, text: 'Plan a solution' },
|
||||
{
|
||||
id: 1,
|
||||
type: 'thinking' as const,
|
||||
thought: {
|
||||
subject: 'Initial analysis',
|
||||
description:
|
||||
'This is a multiple line paragraph for the first thinking message of how the model analyzes the problem.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'thinking' as const,
|
||||
thought: {
|
||||
subject: 'Planning execution',
|
||||
description:
|
||||
'This a second multiple line paragraph for the second thinking message explaining the plan in detail so that it wraps around the terminal display.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'thinking' as const,
|
||||
thought: {
|
||||
subject: 'Refining approach',
|
||||
description:
|
||||
'And finally a third multiple line paragraph for the third thinking message to refine the solution.',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const renderResult = renderWithProviders(<MainContent />, {
|
||||
uiState: uiState as Partial<UIState>,
|
||||
});
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain('Initial analysis');
|
||||
expect(output).toContain('Planning execution');
|
||||
expect(output).toContain('Refining approach');
|
||||
expect(output).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
describe('MainContent Tool Output Height Logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
|
||||
@@ -62,11 +62,31 @@ export const MainContent = () => {
|
||||
return -1;
|
||||
}, [uiState.history]);
|
||||
|
||||
const augmentedHistory = useMemo(
|
||||
() =>
|
||||
uiState.history.map((item, index) => {
|
||||
const isExpandable = index > lastUserPromptIndex;
|
||||
const prevType =
|
||||
index > 0 ? uiState.history[index - 1]?.type : undefined;
|
||||
const isFirstThinking =
|
||||
item.type === 'thinking' && prevType !== 'thinking';
|
||||
const isFirstAfterThinking =
|
||||
item.type !== 'thinking' && prevType === 'thinking';
|
||||
|
||||
return {
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
};
|
||||
}),
|
||||
[uiState.history, lastUserPromptIndex],
|
||||
);
|
||||
|
||||
const historyItems = useMemo(
|
||||
() =>
|
||||
uiState.history.map((h, index) => {
|
||||
const isExpandable = index > lastUserPromptIndex;
|
||||
return (
|
||||
augmentedHistory.map(
|
||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => (
|
||||
<MemoizedHistoryItemDisplay
|
||||
terminalWidth={mainAreaWidth}
|
||||
availableTerminalHeight={
|
||||
@@ -75,21 +95,22 @@ export const MainContent = () => {
|
||||
: undefined
|
||||
}
|
||||
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
|
||||
key={h.id}
|
||||
item={h}
|
||||
key={item.id}
|
||||
item={item}
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
isExpandable={isExpandable}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
[
|
||||
uiState.history,
|
||||
augmentedHistory,
|
||||
mainAreaWidth,
|
||||
staticAreaMaxItemHeight,
|
||||
uiState.slashCommands,
|
||||
uiState.constrainHeight,
|
||||
lastUserPromptIndex,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -106,18 +127,31 @@ export const MainContent = () => {
|
||||
const pendingItems = useMemo(
|
||||
() => (
|
||||
<Box flexDirection="column">
|
||||
{pendingHistoryItems.map((item, i) => (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? staticAreaMaxItemHeight : undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
isExpandable={true}
|
||||
/>
|
||||
))}
|
||||
{pendingHistoryItems.map((item, i) => {
|
||||
const prevType =
|
||||
i === 0
|
||||
? uiState.history.at(-1)?.type
|
||||
: pendingHistoryItems[i - 1]?.type;
|
||||
const isFirstThinking =
|
||||
item.type === 'thinking' && prevType !== 'thinking';
|
||||
const isFirstAfterThinking =
|
||||
item.type !== 'thinking' && prevType === 'thinking';
|
||||
|
||||
return (
|
||||
<HistoryItemDisplay
|
||||
key={i}
|
||||
availableTerminalHeight={
|
||||
uiState.constrainHeight ? staticAreaMaxItemHeight : undefined
|
||||
}
|
||||
terminalWidth={mainAreaWidth}
|
||||
item={{ ...item, id: 0 }}
|
||||
isPending={true}
|
||||
isExpandable={true}
|
||||
isFirstThinking={isFirstThinking}
|
||||
isFirstAfterThinking={isFirstAfterThinking}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showConfirmationQueue && confirmingTool && (
|
||||
<ToolConfirmationQueue confirmingTool={confirmingTool} />
|
||||
)}
|
||||
@@ -130,20 +164,25 @@ export const MainContent = () => {
|
||||
mainAreaWidth,
|
||||
showConfirmationQueue,
|
||||
confirmingTool,
|
||||
uiState.history,
|
||||
],
|
||||
);
|
||||
|
||||
const virtualizedData = useMemo(
|
||||
() => [
|
||||
{ type: 'header' as const },
|
||||
...uiState.history.map((item, index) => ({
|
||||
type: 'history' as const,
|
||||
item,
|
||||
isExpandable: index > lastUserPromptIndex,
|
||||
})),
|
||||
...augmentedHistory.map(
|
||||
({ item, isExpandable, isFirstThinking, isFirstAfterThinking }) => ({
|
||||
type: 'history' as const,
|
||||
item,
|
||||
isExpandable,
|
||||
isFirstThinking,
|
||||
isFirstAfterThinking,
|
||||
}),
|
||||
),
|
||||
{ type: 'pending' as const },
|
||||
],
|
||||
[uiState.history, lastUserPromptIndex],
|
||||
[augmentedHistory],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
@@ -171,6 +210,8 @@ export const MainContent = () => {
|
||||
isPending={false}
|
||||
commands={uiState.slashCommands}
|
||||
isExpandable={item.isExpandable}
|
||||
isFirstThinking={item.isFirstThinking}
|
||||
isFirstAfterThinking={item.isFirstAfterThinking}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
|
||||
@@ -115,20 +115,6 @@ Review your answers:
|
||||
Tests → (not answered)
|
||||
Docs → (not answered)
|
||||
|
||||
Enter to submit · / to edit answers · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > allows navigating to Review tab and back 2`] = `
|
||||
"← □ Tests │ □ Docs │ ≡ Review →
|
||||
|
||||
Review your answers:
|
||||
|
||||
⚠ You have 2 unanswered questions
|
||||
|
||||
Tests → (not answered)
|
||||
Docs → (not answered)
|
||||
|
||||
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
|
||||
"
|
||||
`;
|
||||
@@ -212,20 +198,6 @@ Review your answers:
|
||||
License → (not answered)
|
||||
README → (not answered)
|
||||
|
||||
Enter to submit · / to edit answers · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > shows warning for unanswered questions on Review tab 2`] = `
|
||||
"← □ License │ □ README │ ≡ Review →
|
||||
|
||||
Review your answers:
|
||||
|
||||
⚠ You have 2 unanswered questions
|
||||
|
||||
License → (not answered)
|
||||
README → (not answered)
|
||||
|
||||
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -388,8 +388,17 @@ exports[`<HistoryItemDisplay /> > renders InfoMessage for "info" type with multi
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<HistoryItemDisplay /> > thinking items > renders thinking item when enabled 1`] = `
|
||||
" Thinking
|
||||
exports[`<HistoryItemDisplay /> > thinking items > renders "Thinking..." header when isFirstThinking is true 1`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Thinking
|
||||
│ test
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`<HistoryItemDisplay /> > thinking items > renders thinking item when enabled 1`] = `
|
||||
" │
|
||||
│ Thinking
|
||||
│ test
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="326" viewBox="0 0 920 326">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="326" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">ScrollableList </text>
|
||||
<text x="0" y="19" fill="#ffffff" textLength="900" lengthAdjust="spacingAndGlyphs">AppHeader(full) </text>
|
||||
<rect x="0" y="34" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="36" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀</text>
|
||||
<rect x="0" y="51" width="9" height="17" fill="#141414" />
|
||||
<rect x="9" y="51" width="18" height="17" fill="#141414" />
|
||||
<text x="9" y="53" fill="#d7afff" textLength="18" lengthAdjust="spacingAndGlyphs">> </text>
|
||||
<rect x="27" y="51" width="135" height="17" fill="#141414" />
|
||||
<text x="27" y="53" fill="#ffffff" textLength="135" lengthAdjust="spacingAndGlyphs">Plan a solution</text>
|
||||
<rect x="162" y="51" width="738" height="17" fill="#141414" />
|
||||
<rect x="0" y="68" width="900" height="17" fill="#141414" />
|
||||
<text x="0" y="70" fill="#000000" textLength="900" lengthAdjust="spacingAndGlyphs">▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄</text>
|
||||
<text x="0" y="87" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="121" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="121" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Initial analysis</text>
|
||||
<text x="9" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="138" fill="#afafaf" textLength="846" lengthAdjust="spacingAndGlyphs" font-style="italic">This is a multiple line paragraph for the first thinking message of how the model analyzes the</text>
|
||||
<text x="9" y="155" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="155" fill="#afafaf" textLength="72" lengthAdjust="spacingAndGlyphs" font-style="italic">problem.</text>
|
||||
<text x="9" y="172" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="189" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="189" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Planning execution</text>
|
||||
<text x="9" y="206" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="206" fill="#afafaf" textLength="828" lengthAdjust="spacingAndGlyphs" font-style="italic">This a second multiple line paragraph for the second thinking message explaining the plan in</text>
|
||||
<text x="9" y="223" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="223" fill="#afafaf" textLength="468" lengthAdjust="spacingAndGlyphs" font-style="italic">detail so that it wraps around the terminal display.</text>
|
||||
<text x="9" y="240" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="257" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="257" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Refining approach</text>
|
||||
<text x="9" y="274" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="274" fill="#afafaf" textLength="792" lengthAdjust="spacingAndGlyphs" font-style="italic">And finally a third multiple line paragraph for the third thinking message to refine the</text>
|
||||
<text x="9" y="291" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="291" fill="#afafaf" textLength="81" lengthAdjust="spacingAndGlyphs" font-style="italic">solution.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -151,3 +151,46 @@ AppHeader(full)
|
||||
Gemini message 2
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`MainContent > renders multiple thinking messages sequentially correctly 1`] = `
|
||||
"ScrollableList
|
||||
AppHeader(full)
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Plan a solution
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
Thinking...
|
||||
│
|
||||
│ Initial analysis
|
||||
│ This is a multiple line paragraph for the first thinking message of how the model analyzes the
|
||||
│ problem.
|
||||
│
|
||||
│ Planning execution
|
||||
│ This a second multiple line paragraph for the second thinking message explaining the plan in
|
||||
│ detail so that it wraps around the terminal display.
|
||||
│
|
||||
│ Refining approach
|
||||
│ And finally a third multiple line paragraph for the third thinking message to refine the
|
||||
│ solution.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`MainContent > renders multiple thinking messages sequentially correctly 2`] = `
|
||||
"ScrollableList
|
||||
AppHeader(full)
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
> Plan a solution
|
||||
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
||||
Thinking...
|
||||
│
|
||||
│ Initial analysis
|
||||
│ This is a multiple line paragraph for the first thinking message of how the model analyzes the
|
||||
│ problem.
|
||||
│
|
||||
│ Planning execution
|
||||
│ This a second multiple line paragraph for the second thinking message explaining the plan in
|
||||
│ detail so that it wraps around the terminal display.
|
||||
│
|
||||
│ Refining approach
|
||||
│ And finally a third multiple line paragraph for the third thinking message to refine the
|
||||
│ solution."
|
||||
`;
|
||||
|
||||
@@ -7,84 +7,156 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { ThinkingMessage } from './ThinkingMessage.js';
|
||||
import React from 'react';
|
||||
|
||||
describe('ThinkingMessage', () => {
|
||||
it('renders subject line', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
it('renders subject line with vertical rule and "Thinking..." header', async () => {
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{ subject: 'Planning', description: 'test' }}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain(' Thinking...');
|
||||
expect(output).toContain('│');
|
||||
expect(output).toContain('Planning');
|
||||
expect(output).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('uses description when subject is empty', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{ subject: '', description: 'Processing details' }}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain('Processing details');
|
||||
expect(output).toContain('│');
|
||||
expect(output).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('renders full mode with left border and full text', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Planning',
|
||||
description: 'I am planning the solution.',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain('│');
|
||||
expect(output).toContain('Planning');
|
||||
expect(output).toContain('I am planning the solution.');
|
||||
expect(output).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('indents summary line correctly', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
it('renders "Thinking..." header when isFirstThinking is true', async () => {
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Summary line',
|
||||
description: 'First body line',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
const output = renderResult.lastFrame();
|
||||
expect(output).toContain(' Thinking...');
|
||||
expect(output).toContain('Summary line');
|
||||
expect(output).toContain('│');
|
||||
expect(output).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('normalizes escaped newline tokens', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Matching the Blocks',
|
||||
description: '\\n\\nSome more text',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
expect(renderResult.lastFrame()).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('renders empty state gracefully', async () => {
|
||||
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||
<ThinkingMessage thought={{ subject: '', description: '' }} />,
|
||||
const renderResult = renderWithProviders(
|
||||
<ThinkingMessage
|
||||
thought={{ subject: '', description: '' }}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
expect(renderResult.lastFrame({ allowEmpty: true })).toBe('');
|
||||
renderResult.unmount();
|
||||
});
|
||||
|
||||
it('renders multiple thinking messages sequentially correctly', async () => {
|
||||
const renderResult = renderWithProviders(
|
||||
<React.Fragment>
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Initial analysis',
|
||||
description:
|
||||
'This is a multiple line paragraph for the first thinking message of how the model analyzes the problem.',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
isFirstThinking={true}
|
||||
/>
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Planning execution',
|
||||
description:
|
||||
'This a second multiple line paragraph for the second thinking message explaining the plan in detail so that it wraps around the terminal display.',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
/>
|
||||
<ThinkingMessage
|
||||
thought={{
|
||||
subject: 'Refining approach',
|
||||
description:
|
||||
'And finally a third multiple line paragraph for the third thinking message to refine the solution.',
|
||||
}}
|
||||
terminalWidth={80}
|
||||
/>
|
||||
</React.Fragment>,
|
||||
);
|
||||
await renderResult.waitUntilReady();
|
||||
|
||||
expect(renderResult.lastFrame()).toMatchSnapshot();
|
||||
await expect(renderResult).toMatchSvgSnapshot();
|
||||
renderResult.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,30 @@ import { normalizeEscapedNewlines } from '../../utils/textUtils.js';
|
||||
|
||||
interface ThinkingMessageProps {
|
||||
thought: ThoughtSummary;
|
||||
terminalWidth: number;
|
||||
isFirstThinking?: boolean;
|
||||
}
|
||||
|
||||
const THINKING_LEFT_PADDING = 1;
|
||||
|
||||
function normalizeThoughtLines(thought: ThoughtSummary): string[] {
|
||||
const subject = normalizeEscapedNewlines(thought.subject).trim();
|
||||
const description = normalizeEscapedNewlines(thought.description).trim();
|
||||
|
||||
if (!subject && !description) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!subject) {
|
||||
return description.split('\n');
|
||||
}
|
||||
|
||||
if (!description) {
|
||||
return [subject];
|
||||
}
|
||||
|
||||
const bodyLines = description.split('\n');
|
||||
return [subject, ...bodyLines];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,60 +45,47 @@ interface ThinkingMessageProps {
|
||||
*/
|
||||
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
|
||||
thought,
|
||||
terminalWidth,
|
||||
isFirstThinking,
|
||||
}) => {
|
||||
const { summary, body } = useMemo(() => {
|
||||
const subject = normalizeEscapedNewlines(thought.subject).trim();
|
||||
const description = normalizeEscapedNewlines(thought.description).trim();
|
||||
const fullLines = useMemo(() => normalizeThoughtLines(thought), [thought]);
|
||||
|
||||
if (!subject && !description) {
|
||||
return { summary: '', body: '' };
|
||||
}
|
||||
|
||||
if (!subject) {
|
||||
const lines = description
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
return {
|
||||
summary: lines[0] || '',
|
||||
body: lines.slice(1).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
summary: subject,
|
||||
body: description,
|
||||
};
|
||||
}, [thought]);
|
||||
|
||||
if (!summary && !body) {
|
||||
if (fullLines.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width="100%" marginBottom={1} paddingLeft={1} flexDirection="column">
|
||||
{summary && (
|
||||
<Box paddingLeft={2}>
|
||||
<Box width={terminalWidth} flexDirection="column">
|
||||
{isFirstThinking && (
|
||||
<Text color={theme.text.primary} italic>
|
||||
{' '}
|
||||
Thinking...{' '}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box
|
||||
marginLeft={THINKING_LEFT_PADDING}
|
||||
paddingLeft={1}
|
||||
borderStyle="single"
|
||||
borderLeft={true}
|
||||
borderRight={false}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
borderColor={theme.text.secondary}
|
||||
flexDirection="column"
|
||||
>
|
||||
<Text> </Text>
|
||||
{fullLines.length > 0 && (
|
||||
<Text color={theme.text.primary} bold italic>
|
||||
{summary}
|
||||
{fullLines[0]}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{body && (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
borderLeft
|
||||
borderRight={false}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
borderColor={theme.border.default}
|
||||
paddingLeft={1}
|
||||
>
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{body}
|
||||
)}
|
||||
{fullLines.slice(1).map((line, index) => (
|
||||
<Text key={`body-line-${index}`} color={theme.text.secondary} italic>
|
||||
{line}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="171" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Matching the Blocks</text>
|
||||
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="53" fill="#afafaf" textLength="126" lengthAdjust="spacingAndGlyphs" font-style="italic">Some more text</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="108" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Summary line</text>
|
||||
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="53" fill="#afafaf" textLength="135" lengthAdjust="spacingAndGlyphs" font-style="italic">First body line</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Planning</text>
|
||||
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="53" fill="#afafaf" textLength="243" lengthAdjust="spacingAndGlyphs" font-style="italic">I am planning the solution.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,30 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="241" viewBox="0 0 920 241">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="241" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="144" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Initial analysis</text>
|
||||
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="53" fill="#afafaf" textLength="675" lengthAdjust="spacingAndGlyphs" font-style="italic">This is a multiple line paragraph for the first thinking message of how the</text>
|
||||
<text x="9" y="70" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="70" fill="#afafaf" textLength="243" lengthAdjust="spacingAndGlyphs" font-style="italic">model analyzes the problem.</text>
|
||||
<text x="9" y="87" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="104" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="104" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Planning execution</text>
|
||||
<text x="9" y="121" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="121" fill="#afafaf" textLength="621" lengthAdjust="spacingAndGlyphs" font-style="italic">This a second multiple line paragraph for the second thinking message</text>
|
||||
<text x="9" y="138" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="138" fill="#afafaf" textLength="675" lengthAdjust="spacingAndGlyphs" font-style="italic">explaining the plan in detail so that it wraps around the terminal display.</text>
|
||||
<text x="9" y="155" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="172" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="172" fill="#ffffff" textLength="153" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Refining approach</text>
|
||||
<text x="9" y="189" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="189" fill="#afafaf" textLength="693" lengthAdjust="spacingAndGlyphs" font-style="italic">And finally a third multiple line paragraph for the third thinking message to</text>
|
||||
<text x="9" y="206" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="206" fill="#afafaf" textLength="180" lengthAdjust="spacingAndGlyphs" font-style="italic">refine the solution.</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="88" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="72" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Planning</text>
|
||||
<text x="9" y="53" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="53" fill="#afafaf" textLength="36" lengthAdjust="spacingAndGlyphs" font-style="italic">test</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="71" viewBox="0 0 920 71">
|
||||
<style>
|
||||
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
|
||||
</style>
|
||||
<rect width="920" height="71" fill="#000000" />
|
||||
<g transform="translate(10, 10)">
|
||||
<text x="0" y="2" fill="#ffffff" textLength="117" lengthAdjust="spacingAndGlyphs" font-style="italic"> Thinking... </text>
|
||||
<text x="9" y="19" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="9" y="36" fill="#afafaf" textLength="9" lengthAdjust="spacingAndGlyphs">│</text>
|
||||
<text x="27" y="36" fill="#ffffff" textLength="162" lengthAdjust="spacingAndGlyphs" font-weight="bold" font-style="italic">Processing details</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 812 B |
@@ -1,30 +1,107 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ThinkingMessage > indents summary line correctly 1`] = `
|
||||
" Summary line
|
||||
│ First body line
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > normalizes escaped newline tokens 1`] = `
|
||||
" Matching the Blocks
|
||||
" Thinking...
|
||||
│
|
||||
│ Matching the Blocks
|
||||
│ Some more text
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > normalizes escaped newline tokens 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Matching the Blocks
|
||||
│ Some more text"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders "Thinking..." header when isFirstThinking is true 1`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Summary line
|
||||
│ First body line
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders "Thinking..." header when isFirstThinking is true 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Summary line
|
||||
│ First body line"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders full mode with left border and full text 1`] = `
|
||||
" Planning
|
||||
" Thinking...
|
||||
│
|
||||
│ Planning
|
||||
│ I am planning the solution.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders subject line 1`] = `
|
||||
" Planning
|
||||
exports[`ThinkingMessage > renders full mode with left border and full text 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Planning
|
||||
│ I am planning the solution."
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders multiple thinking messages sequentially correctly 1`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Initial analysis
|
||||
│ This is a multiple line paragraph for the first thinking message of how the
|
||||
│ model analyzes the problem.
|
||||
│
|
||||
│ Planning execution
|
||||
│ This a second multiple line paragraph for the second thinking message
|
||||
│ explaining the plan in detail so that it wraps around the terminal display.
|
||||
│
|
||||
│ Refining approach
|
||||
│ And finally a third multiple line paragraph for the third thinking message to
|
||||
│ refine the solution.
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders multiple thinking messages sequentially correctly 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Initial analysis
|
||||
│ This is a multiple line paragraph for the first thinking message of how the
|
||||
│ model analyzes the problem.
|
||||
│
|
||||
│ Planning execution
|
||||
│ This a second multiple line paragraph for the second thinking message
|
||||
│ explaining the plan in detail so that it wraps around the terminal display.
|
||||
│
|
||||
│ Refining approach
|
||||
│ And finally a third multiple line paragraph for the third thinking message to
|
||||
│ refine the solution."
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders subject line with vertical rule and "Thinking..." header 1`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Planning
|
||||
│ test
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > renders subject line with vertical rule and "Thinking..." header 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Planning
|
||||
│ test"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > uses description when subject is empty 1`] = `
|
||||
" Processing details
|
||||
" Thinking...
|
||||
│
|
||||
│ Processing details
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ThinkingMessage > uses description when subject is empty 2`] = `
|
||||
" Thinking...
|
||||
│
|
||||
│ Processing details"
|
||||
`;
|
||||
|
||||