feat: implement /rewind command (#15720)

This commit is contained in:
Adib234
2026-01-22 10:26:52 -05:00
committed by GitHub
parent ff9c77925e
commit 3b9f580fa4
26 changed files with 931 additions and 145 deletions
@@ -2007,7 +2007,9 @@ describe('InputPrompt', () => {
await act(async () => {
stdin.write('\x1B\x1B');
vi.advanceTimersByTime(100);
});
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith('/rewind');
});
unmount();
+14 -12
View File
@@ -30,7 +30,7 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import type { CommandContext, SlashCommand } from '../commands/types.js';
import type { Config } from '@google/gemini-cli-core';
import { ApprovalMode, debugLogger } from '@google/gemini-cli-core';
import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core';
import {
parseInputForHighlighting,
parseSegmentsFromTokens,
@@ -516,18 +516,20 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
escapeTimerRef.current = setTimeout(() => {
resetEscapeState();
}, 500);
} else {
// Second ESC
resetEscapeState();
if (buffer.text.length > 0) {
buffer.setText('');
resetCompletionState();
} else {
if (history.length > 0) {
onSubmit('/rewind');
}
}
return;
}
// Second ESC
resetEscapeState();
if (buffer.text.length > 0) {
buffer.setText('');
resetCompletionState();
return;
} else if (history.length > 0) {
onSubmit('/rewind');
return;
}
coreEvents.emitFeedback('info', 'Nothing to rewind to');
return;
}
@@ -252,17 +252,16 @@ describe('RewindViewer', () => {
it.each([
{
description: 'removes reference markers',
prompt:
'some command @file\n--- Content from referenced files ---\nContent from file:\nblah blah\n--- End of content ---',
prompt: `some command @file\n--- Content from referenced files ---\nContent from file:\nblah blah\n--- End of content ---`,
},
{
description: 'strips expanded MCP resource content',
prompt:
'read @server3:mcp://demo-resource hello\n' +
'--- Content from referenced files ---\n' +
`--- Content from referenced files ---\n` +
'\nContent from @server3:mcp://demo-resource:\n' +
'This is the content of the demo resource.\n' +
'--- End of content ---',
`--- End of content ---`,
},
])('$description', async ({ prompt }) => {
const conversation = createConversation([
+112 -37
View File
@@ -5,7 +5,7 @@
*/
import type React from 'react';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { Box, Text } from 'ink';
import { useUIState } from '../contexts/UIStateContext.js';
import {
@@ -19,8 +19,9 @@ import { useKeypress } from '../hooks/useKeypress.js';
import { useRewind } from '../hooks/useRewind.js';
import { RewindConfirmation, RewindOutcome } from './RewindConfirmation.js';
import { stripReferenceContent } from '../utils/formatters.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
import { keyMatchers, Command } from '../keyMatchers.js';
import { CliSpinner } from './CliSpinner.js';
import { ExpandableText } from './shared/ExpandableText.js';
interface RewindViewerProps {
conversation: ConversationRecord;
@@ -29,7 +30,7 @@ interface RewindViewerProps {
messageId: string,
newText: string,
outcome: RewindOutcome,
) => void;
) => Promise<void>;
}
const MAX_LINES_PER_BOX = 2;
@@ -39,6 +40,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
onExit,
onRewind,
}) => {
const [isRewinding, setIsRewinding] = useState(false);
const { terminalWidth, terminalHeight } = useUIState();
const {
selectedMessageId,
@@ -48,28 +50,58 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
clearSelection,
} = useRewind(conversation);
const [highlightedMessageId, setHighlightedMessageId] = useState<
string | null
>(null);
const [expandedMessageId, setExpandedMessageId] = useState<string | null>(
null,
);
const interactions = useMemo(
() => conversation.messages.filter((msg) => msg.type === 'user'),
[conversation.messages],
);
const items = useMemo(
() =>
interactions
.map((msg, idx) => ({
key: `${msg.id || 'msg'}-${idx}`,
value: msg,
index: idx,
}))
.reverse(),
[interactions],
);
const items = useMemo(() => {
const interactionItems = interactions.map((msg, idx) => ({
key: `${msg.id || 'msg'}-${idx}`,
value: msg,
index: idx,
}));
// Add "Current Position" as the last item
return [
...interactionItems,
{
key: 'current-position',
value: {
id: 'current-position',
type: 'user',
content: 'Stay at current position',
timestamp: new Date().toISOString(),
} as MessageRecord,
index: interactionItems.length,
},
];
}, [interactions]);
useKeypress(
(key) => {
if (!selectedMessageId) {
if (keyMatchers[Command.ESCAPE](key)) {
onExit();
return;
}
if (keyMatchers[Command.EXPAND_SUGGESTION](key)) {
if (
highlightedMessageId &&
highlightedMessageId !== 'current-position'
) {
setExpandedMessageId(highlightedMessageId);
}
}
if (keyMatchers[Command.COLLAPSE_SUGGESTION](key)) {
setExpandedMessageId(null);
}
}
},
@@ -89,6 +121,28 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
const maxItemsToShow = Math.max(1, Math.floor(listHeight / 4));
if (selectedMessageId) {
if (isRewinding) {
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
padding={1}
width={terminalWidth}
flexDirection="row"
>
<Box>
<CliSpinner />
</Box>
<Text>Rewinding...</Text>
</Box>
);
}
if (selectedMessageId === 'current-position') {
onExit();
return null;
}
const selectedMessage = interactions.find(
(m) => m.id === selectedMessageId,
);
@@ -97,7 +151,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
stats={confirmationStats}
terminalWidth={terminalWidth}
timestamp={selectedMessage?.timestamp}
onConfirm={(outcome) => {
onConfirm={async (outcome) => {
if (outcome === RewindOutcome.Cancel) {
clearSelection();
} else {
@@ -109,7 +163,8 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
? partToString(userPrompt.content)
: '';
const cleanedText = stripReferenceContent(originalUserText);
onRewind(selectedMessageId, cleanedText, outcome);
setIsRewinding(true);
await onRewind(selectedMessageId, cleanedText, outcome);
}
}
}}
@@ -138,12 +193,41 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
onSelect={(item: MessageRecord) => {
const userPrompt = item;
if (userPrompt && userPrompt.id) {
selectMessage(userPrompt.id);
if (userPrompt.id === 'current-position') {
onExit();
} else {
selectMessage(userPrompt.id);
}
}
}}
onHighlight={(item: MessageRecord) => {
if (item.id) {
setHighlightedMessageId(item.id);
// Collapse when moving selection
setExpandedMessageId(null);
}
}}
maxItemsToShow={maxItemsToShow}
renderItem={(itemWrapper, { isSelected }) => {
const userPrompt = itemWrapper.value;
if (userPrompt.id === 'current-position') {
return (
<Box flexDirection="column" marginBottom={1}>
<Text
color={
isSelected ? theme.status.success : theme.text.primary
}
>
{partToString(userPrompt.content)}
</Text>
<Text color={theme.text.secondary}>
Cancel rewind and stay here
</Text>
</Box>
);
}
const stats = getStats(userPrompt);
const firstFileName = stats?.details?.at(0)?.fileName;
const originalUserText = userPrompt.content
@@ -154,25 +238,15 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
return (
<Box flexDirection="column" marginBottom={1}>
<Box>
<MaxSizedBox
maxWidth={terminalWidth - 4}
maxHeight={isSelected ? undefined : MAX_LINES_PER_BOX + 1}
overflowDirection="bottom"
>
{cleanedText.split('\n').map((line, i) => (
<Box key={i}>
<Text
color={
isSelected
? theme.status.success
: theme.text.primary
}
>
{line}
</Text>
</Box>
))}
</MaxSizedBox>
<ExpandableText
label={cleanedText}
isExpanded={expandedMessageId === userPrompt.id}
textColor={
isSelected ? theme.status.success : theme.text.primary
}
maxWidth={(terminalWidth - 4) * MAX_LINES_PER_BOX}
maxLines={MAX_LINES_PER_BOX}
/>
</Box>
{stats ? (
<Box flexDirection="row">
@@ -203,7 +277,8 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
<Box marginTop={1}>
<Text color={theme.text.secondary}>
(Use Enter to select a message, Esc to close)
(Use Enter to select a message, Esc to close, Right/Left to
expand/collapse)
</Text>
</Box>
</Box>
@@ -6,7 +6,7 @@
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { PrepareLabel, MAX_WIDTH } from './PrepareLabel.js';
import { ExpandableText, MAX_WIDTH } from './shared/ExpandableText.js';
import { CommandKind } from '../commands/types.js';
import { Colors } from '../colors.js';
export interface Suggestion {
@@ -85,7 +85,7 @@ export function SuggestionsDisplay({
const textColor = isActive ? theme.text.accent : theme.text.secondary;
const isLong = suggestion.value.length >= MAX_WIDTH;
const labelElement = (
<PrepareLabel
<ExpandableText
label={suggestion.value}
matchedIndex={suggestion.matchedIndex}
userInput={userInput}
@@ -8,8 +8,11 @@ exports[`RewindViewer > Content Filtering > 'removes reference markers' 1`] = `
│ ● some command @file │
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -22,8 +25,11 @@ exports[`RewindViewer > Content Filtering > 'strips expanded MCP resource conten
│ ● read @server3:mcp://demo-resource hello │
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -63,17 +69,20 @@ exports[`RewindViewer > Navigation > handles 'down' navigation > after-down 1`]
│ │
│ > Rewind │
│ │
│ Q3
│ Q1
│ No files have been changed │
│ │
│ ● Q2 │
│ No files have been changed │
│ │
│ Q1
│ Q3
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -83,17 +92,20 @@ exports[`RewindViewer > Navigation > handles 'up' navigation > after-up 1`] = `
│ │
│ > Rewind │
│ │
│ Q3
│ Q1
│ No files have been changed │
│ │
│ Q2 │
│ No files have been changed │
│ │
Q1
Q3
│ No files have been changed │
│ │
│ ● Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -103,17 +115,20 @@ exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-down 1`]
│ │
│ > Rewind │
│ │
│ ● Q3
│ ● Q1
│ No files have been changed │
│ │
│ Q2 │
│ No files have been changed │
│ │
│ Q1
│ Q3
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -123,17 +138,20 @@ exports[`RewindViewer > Navigation > handles cyclic navigation > cyclic-up 1`] =
│ │
│ > Rewind │
│ │
│ Q3
│ Q1
│ No files have been changed │
│ │
│ Q2 │
│ No files have been changed │
│ │
Q1
Q3
│ No files have been changed │
│ │
│ ● Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -146,8 +164,11 @@ exports[`RewindViewer > Rendering > renders 'a single interaction' 1`] = `
│ ● Hello │
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -158,16 +179,14 @@ exports[`RewindViewer > Rendering > renders 'full text for selected item' 1`] =
│ > Rewind │
│ │
│ ● 1 │
│ 2
│ 3 │
│ 4 │
│ 5 │
│ 6 │
│ 7 │
│ 2...
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -177,8 +196,11 @@ exports[`RewindViewer > Rendering > renders 'nothing interesting for empty conve
│ │
│ > Rewind │
│ │
│ ● Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -188,14 +210,17 @@ exports[`RewindViewer > updates content when conversation changes (background up
│ │
│ > Rewind │
│ │
│ ● Message 2
│ ● Message 1
│ No files have been changed │
│ │
│ Message 1
│ Message 2
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -208,8 +233,11 @@ exports[`RewindViewer > updates content when conversation changes (background up
│ ● Message 1 │
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -219,22 +247,19 @@ exports[`RewindViewer > updates selection and expansion on navigation > after-do
│ │
│ > Rewind │
│ │
│ Line 1
│ Line 2
│ ... last 5 lines hidden ... │
│ Line A
│ Line B...
│ No files have been changed │
│ │
│ ● Line A
│ Line B
│ Line C │
│ Line D │
│ Line E │
│ Line F │
│ Line G │
│ ● Line 1
│ Line 2...
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -244,22 +269,19 @@ exports[`RewindViewer > updates selection and expansion on navigation > initial-
│ │
│ > Rewind │
│ │
│ ● Line 1
│ Line 2
│ Line 3 │
│ Line 4 │
│ Line 5 │
│ Line 6 │
│ Line 7 │
│ ● Line A
│ Line B...
│ No files have been changed │
│ │
│ Line A
│ Line B
│ ... last 5 lines hidden ... │
│ Line 1
│ Line 2...
│ No files have been changed │
│ │
│ Stay at current position │
│ Cancel rewind and stay here │
│ │
(Use Enter to select a message, Esc to close)
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -1,20 +1,20 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { render } from '../../test-utils/render.js';
import { PrepareLabel, MAX_WIDTH } from './PrepareLabel.js';
import { render } from '../../../test-utils/render.js';
import { ExpandableText, MAX_WIDTH } from './ExpandableText.js';
describe('PrepareLabel', () => {
describe('ExpandableText', () => {
const color = 'white';
const flat = (s: string | undefined) => (s ?? '').replace(/\n/g, '');
it('renders plain label when no match (short label)', () => {
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label="simple command"
userInput=""
matchedIndex={undefined}
@@ -29,7 +29,7 @@ describe('PrepareLabel', () => {
it('truncates long label when collapsed and no match', () => {
const long = 'x'.repeat(MAX_WIDTH + 25);
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label={long}
userInput=""
textColor={color}
@@ -47,7 +47,7 @@ describe('PrepareLabel', () => {
it('shows full long label when expanded and no match', () => {
const long = 'y'.repeat(MAX_WIDTH + 25);
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label={long}
userInput=""
textColor={color}
@@ -66,7 +66,7 @@ describe('PrepareLabel', () => {
const userInput = 'commit';
const matchedIndex = label.indexOf(userInput);
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label={label}
userInput={userInput}
matchedIndex={matchedIndex}
@@ -86,7 +86,7 @@ describe('PrepareLabel', () => {
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label={label}
userInput={core}
matchedIndex={matchedIndex}
@@ -111,7 +111,7 @@ describe('PrepareLabel', () => {
const label = prefix + core + suffix;
const matchedIndex = prefix.length;
const { lastFrame, unmount } = render(
<PrepareLabel
<ExpandableText
label={label}
userInput={core}
matchedIndex={matchedIndex}
@@ -128,4 +128,24 @@ describe('PrepareLabel', () => {
expect(out).toMatchSnapshot();
unmount();
});
it('respects custom maxWidth', () => {
const customWidth = 50;
const long = 'z'.repeat(100);
const { lastFrame, unmount } = render(
<ExpandableText
label={long}
userInput=""
textColor={color}
isExpanded={false}
maxWidth={customWidth}
/>,
);
const out = lastFrame();
const f = flat(out);
expect(f.endsWith('...')).toBe(true);
expect(f.length).toBe(customWidth + 3);
expect(out).toMatchSnapshot();
unmount();
});
});
@@ -1,29 +1,33 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { theme } from '../../semantic-colors.js';
export const MAX_WIDTH = 150; // Maximum width for the text that is shown
export const MAX_WIDTH = 150;
export interface PrepareLabelProps {
export interface ExpandableTextProps {
label: string;
matchedIndex?: number;
userInput: string;
textColor: string;
userInput?: string;
textColor?: string;
isExpanded?: boolean;
maxWidth?: number;
maxLines?: number;
}
const _PrepareLabel: React.FC<PrepareLabelProps> = ({
const _ExpandableText: React.FC<ExpandableTextProps> = ({
label,
matchedIndex,
userInput,
textColor,
userInput = '',
textColor = theme.text.primary,
isExpanded = false,
maxWidth = MAX_WIDTH,
maxLines,
}) => {
const hasMatch =
matchedIndex !== undefined &&
@@ -33,11 +37,27 @@ const _PrepareLabel: React.FC<PrepareLabelProps> = ({
// Render the plain label if there's no match
if (!hasMatch) {
const display = isExpanded
? label
: label.length > MAX_WIDTH
? label.slice(0, MAX_WIDTH) + '...'
: label;
let display = label;
if (!isExpanded) {
if (maxLines !== undefined) {
const lines = label.split('\n');
// 1. Truncate by logical lines
let truncated = lines.slice(0, maxLines).join('\n');
const hasMoreLines = lines.length > maxLines;
// 2. Truncate by characters (visual approximation) to prevent massive wrapping
if (truncated.length > maxWidth) {
truncated = truncated.slice(0, maxWidth) + '...';
} else if (hasMoreLines) {
truncated += '...';
}
display = truncated;
} else if (label.length > maxWidth) {
display = label.slice(0, maxWidth) + '...';
}
}
return (
<Text wrap="wrap" color={textColor}>
{display}
@@ -51,18 +71,18 @@ const _PrepareLabel: React.FC<PrepareLabelProps> = ({
let after = '';
// Case 1: Show the full string if it's expanded or already fits
if (isExpanded || label.length <= MAX_WIDTH) {
if (isExpanded || label.length <= maxWidth) {
before = label.slice(0, matchedIndex);
match = label.slice(matchedIndex, matchedIndex + matchLength);
after = label.slice(matchedIndex + matchLength);
}
// Case 2: The match itself is too long, so we only show a truncated portion of the match
else if (matchLength >= MAX_WIDTH) {
match = label.slice(matchedIndex, matchedIndex + MAX_WIDTH - 1) + '...';
else if (matchLength >= maxWidth) {
match = label.slice(matchedIndex, matchedIndex + maxWidth - 1) + '...';
}
// Case 3: Truncate the string to create a window around the match
else {
const contextSpace = MAX_WIDTH - matchLength;
const contextSpace = maxWidth - matchLength;
const beforeSpace = Math.floor(contextSpace / 2);
const afterSpace = Math.ceil(contextSpace / 2);
@@ -113,4 +133,4 @@ const _PrepareLabel: React.FC<PrepareLabelProps> = ({
);
};
export const PrepareLabel = React.memo(_PrepareLabel);
export const ExpandableText = React.memo(_ExpandableText);
@@ -0,0 +1,27 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ExpandablePrompt > creates centered window around match when collapsed 1`] = `
"...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/
components//and/then/some/more/components//and/..."
`;
exports[`ExpandablePrompt > highlights matched substring when expanded (text only visible) 1`] = `"run: git commit -m "feat: add search""`;
exports[`ExpandablePrompt > renders plain label when no match (short label) 1`] = `"simple command"`;
exports[`ExpandablePrompt > respects custom maxWidth 1`] = `"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."`;
exports[`ExpandablePrompt > shows full long label when expanded and no match 1`] = `
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
`;
exports[`ExpandablePrompt > truncates long label when collapsed and no match 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;
exports[`ExpandablePrompt > truncates match itself when match is very long 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;
@@ -0,0 +1,27 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ExpandableText > creates centered window around match when collapsed 1`] = `
"...ry/long/path/that/keeps/going/cd_/very/long/path/that/keeps/going/search-here/and/then/some/more/
components//and/then/some/more/components//and/..."
`;
exports[`ExpandableText > highlights matched substring when expanded (text only visible) 1`] = `"run: git commit -m "feat: add search""`;
exports[`ExpandableText > renders plain label when no match (short label) 1`] = `"simple command"`;
exports[`ExpandableText > respects custom maxWidth 1`] = `"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."`;
exports[`ExpandableText > shows full long label when expanded and no match 1`] = `
"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
`;
exports[`ExpandableText > truncates long label when collapsed and no match 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;
exports[`ExpandableText > truncates match itself when match is very long 1`] = `
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..."
`;