import figures from 'figures'
import { memo, type ReactNode } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { stringWidth } from '../../ink/stringWidth.js'
import { Box, Text } from '../../ink.js'
import { truncatePathMiddle, truncateToWidth } from '../../utils/format.js'
import type { Theme } from '../../utils/theme.js'
export type SuggestionItem = {
id: string
displayText: string
tag?: string
description?: string
metadata?: unknown
color?: keyof Theme
}
export type SuggestionType =
| 'command'
| 'file'
| 'directory'
| 'agent'
| 'shell'
| 'custom-title'
| 'slack-channel'
| 'none'
export const OVERLAY_MAX_ITEMS = 5
const SELECTED_PREFIX = `${figures.pointer} `
const UNSELECTED_PREFIX = ' '
const PREFIX_WIDTH = stringWidth(SELECTED_PREFIX)
function getIcon(itemId: string): string {
if (itemId.startsWith('file-')) return '+'
if (itemId.startsWith('mcp-resource-')) return '◇'
if (itemId.startsWith('agent-')) return '*'
return '+'
}
function isUnifiedSuggestion(itemId: string): boolean {
return (
itemId.startsWith('file-') ||
itemId.startsWith('mcp-resource-') ||
itemId.startsWith('agent-')
)
}
const SuggestionItemRow = memo(function SuggestionItemRow({
item,
maxColumnWidth,
isSelected,
}: {
item: SuggestionItem
maxColumnWidth?: number
isSelected: boolean
}): ReactNode {
const columns = useTerminalSize().columns
const selectionPrefix = isSelected ? SELECTED_PREFIX : UNSELECTED_PREFIX
const rowBackgroundColor: keyof Theme | undefined = isSelected
? 'suggestion'
: undefined
const textColor: keyof Theme | undefined = isSelected ? 'inverseText' : undefined
if (isUnifiedSuggestion(item.id)) {
const icon = getIcon(item.id)
const dimColor = !isSelected
const isFile = item.id.startsWith('file-')
const isMcpResource = item.id.startsWith('mcp-resource-')
const iconWidth = 2
const paddingWidth = 4
const separatorWidth = item.description ? 3 : 0
let displayText: string
if (isFile) {
const descReserve = item.description
? Math.min(20, stringWidth(item.description))
: 0
const maxPathLength =
columns -
PREFIX_WIDTH -
iconWidth -
paddingWidth -
separatorWidth -
descReserve
displayText = truncatePathMiddle(item.displayText, maxPathLength)
} else if (isMcpResource) {
displayText = truncateToWidth(item.displayText, 30)
} else {
displayText = item.displayText
}
const availableWidth =
columns -
PREFIX_WIDTH -
iconWidth -
stringWidth(displayText) -
separatorWidth -
paddingWidth
let lineContent: string
if (item.description) {
const truncatedDesc = truncateToWidth(
item.description.replace(/\s+/g, ' '),
Math.max(0, availableWidth),
)
lineContent = `${selectionPrefix}${icon} ${displayText} - ${truncatedDesc}`
} else {
lineContent = `${selectionPrefix}${icon} ${displayText}`
}
return (
{lineContent}
)
}
const maxNameWidth = Math.floor(columns * 0.4)
const displayTextWidth = Math.min(
maxColumnWidth ?? stringWidth(item.displayText) + 5,
maxNameWidth,
)
let displayText = item.displayText
if (stringWidth(displayText) > displayTextWidth - 2) {
displayText = truncateToWidth(displayText, displayTextWidth - 2)
}
const paddedDisplayText =
selectionPrefix +
displayText +
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
const tagText = item.tag ? `[${item.tag}] ` : ''
const tagWidth = stringWidth(tagText)
const descriptionWidth = Math.max(
0,
columns - PREFIX_WIDTH - displayTextWidth - tagWidth - 4,
)
const truncatedDescription = item.description
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
: ''
const lineContent = `${paddedDisplayText}${tagText}${truncatedDescription}`
return (
{lineContent}
)
})
type Props = {
suggestions: SuggestionItem[]
selectedSuggestion: number
maxColumnWidth?: number
overlay?: boolean
}
export function PromptInputFooterSuggestions({
suggestions,
selectedSuggestion,
maxColumnWidth: maxColumnWidthProp,
overlay,
}: Props): ReactNode {
const { rows } = useTerminalSize()
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3))
if (suggestions.length === 0) {
return null
}
const maxColumnWidth =
maxColumnWidthProp ??
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
const startIndex = Math.max(
0,
Math.min(
selectedSuggestion - Math.floor(maxVisibleItems / 2),
suggestions.length - maxVisibleItems,
),
)
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length)
const visibleItems = suggestions.slice(startIndex, endIndex)
return (
{visibleItems.map(item => (
))}
)
}
export default memo(PromptInputFooterSuggestions)