mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)
纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,292 +1,248 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
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';
|
||||
import * as React from 'react'
|
||||
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;
|
||||
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
|
||||
|
||||
/**
|
||||
* Get the icon for a suggestion based on its type
|
||||
* Icons: + for files, ◇ for MCP resources, * for agents
|
||||
*/
|
||||
function getIcon(itemId: string): string {
|
||||
if (itemId.startsWith('file-')) return '+';
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇';
|
||||
if (itemId.startsWith('agent-')) return '*';
|
||||
return '+';
|
||||
if (itemId.startsWith('file-')) return '+'
|
||||
if (itemId.startsWith('mcp-resource-')) return '◇'
|
||||
if (itemId.startsWith('agent-')) return '*'
|
||||
return '+'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a unified suggestion type (file, mcp-resource, or agent)
|
||||
*/
|
||||
function isUnifiedSuggestion(itemId: string): boolean {
|
||||
return itemId.startsWith('file-') || itemId.startsWith('mcp-resource-') || itemId.startsWith('agent-');
|
||||
return (
|
||||
itemId.startsWith('file-') ||
|
||||
itemId.startsWith('mcp-resource-') ||
|
||||
itemId.startsWith('agent-')
|
||||
)
|
||||
}
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow(t0: { item: SuggestionItem; maxColumnWidth: number; isSelected: boolean }) {
|
||||
const $ = _c(36);
|
||||
const {
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected
|
||||
} = t0;
|
||||
const columns = useTerminalSize().columns;
|
||||
const isUnified = isUnifiedSuggestion(item.id);
|
||||
|
||||
const SuggestionItemRow = memo(function SuggestionItemRow({
|
||||
item,
|
||||
maxColumnWidth,
|
||||
isSelected,
|
||||
}: {
|
||||
item: SuggestionItem
|
||||
maxColumnWidth?: number
|
||||
isSelected: boolean
|
||||
}): ReactNode {
|
||||
const columns = useTerminalSize().columns
|
||||
const isUnified = isUnifiedSuggestion(item.id)
|
||||
|
||||
// For unified suggestions (file, mcp-resource, agent), use single-line layout with icon
|
||||
if (isUnified) {
|
||||
let t1;
|
||||
if ($[0] !== item.id) {
|
||||
t1 = getIcon(item.id);
|
||||
$[0] = item.id;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const icon = t1;
|
||||
const textColor = isSelected ? "suggestion" : undefined;
|
||||
const dimColor = !isSelected;
|
||||
const isFile = item.id.startsWith("file-");
|
||||
const isMcpResource = item.id.startsWith("mcp-resource-");
|
||||
const separatorWidth = item.description ? 3 : 0;
|
||||
let displayText;
|
||||
const icon = getIcon(item.id)
|
||||
const textColor: keyof Theme | undefined = isSelected
|
||||
? 'suggestion'
|
||||
: undefined
|
||||
const dimColor = !isSelected
|
||||
|
||||
const isFile = item.id.startsWith('file-')
|
||||
const isMcpResource = item.id.startsWith('mcp-resource-')
|
||||
|
||||
// Calculate layout widths
|
||||
// Layout: "X " (2) + displayText + " – " (3) + description + padding (4)
|
||||
const iconWidth = 2 // icon + space (fixed)
|
||||
const paddingWidth = 4
|
||||
const separatorWidth = item.description ? 3 : 0 // ' – ' separator
|
||||
|
||||
// For files, truncate middle of path to show both directory context and filename
|
||||
// For MCP resources, limit displayText to 30 chars (truncate from end)
|
||||
// For agents, no truncation
|
||||
let displayText: string
|
||||
if (isFile) {
|
||||
let t2;
|
||||
if ($[2] !== item.description) {
|
||||
t2 = item.description ? Math.min(20, stringWidth(item.description)) : 0;
|
||||
$[2] = item.description;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const descReserve = t2;
|
||||
const maxPathLength = columns - 2 - 4 - separatorWidth - descReserve;
|
||||
let t3;
|
||||
if ($[4] !== item.displayText || $[5] !== maxPathLength) {
|
||||
t3 = truncatePathMiddle(item.displayText, maxPathLength);
|
||||
$[4] = item.displayText;
|
||||
$[5] = maxPathLength;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
displayText = t3;
|
||||
// Reserve space for description if present, otherwise use all available space
|
||||
const descReserve = item.description
|
||||
? Math.min(20, stringWidth(item.description))
|
||||
: 0
|
||||
const maxPathLength =
|
||||
columns - iconWidth - paddingWidth - separatorWidth - descReserve
|
||||
displayText = truncatePathMiddle(item.displayText, maxPathLength)
|
||||
} else if (isMcpResource) {
|
||||
const maxDisplayTextLength = 30
|
||||
displayText = truncateToWidth(item.displayText, maxDisplayTextLength)
|
||||
} else {
|
||||
if (isMcpResource) {
|
||||
let t2;
|
||||
if ($[7] !== item.displayText) {
|
||||
t2 = truncateToWidth(item.displayText, 30);
|
||||
$[7] = item.displayText;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
displayText = t2;
|
||||
} else {
|
||||
displayText = item.displayText;
|
||||
}
|
||||
displayText = item.displayText
|
||||
}
|
||||
const availableWidth = columns - 2 - stringWidth(displayText) - separatorWidth - 4;
|
||||
let lineContent;
|
||||
|
||||
const availableWidth =
|
||||
columns -
|
||||
iconWidth -
|
||||
stringWidth(displayText) -
|
||||
separatorWidth -
|
||||
paddingWidth
|
||||
|
||||
// Build the full line as a single string to prevent wrapping
|
||||
let lineContent: string
|
||||
if (item.description) {
|
||||
const maxDescLength = Math.max(0, availableWidth);
|
||||
let t2;
|
||||
if ($[9] !== item.description || $[10] !== maxDescLength) {
|
||||
t2 = truncateToWidth(item.description.replace(/\s+/g, " "), maxDescLength);
|
||||
$[9] = item.description;
|
||||
$[10] = maxDescLength;
|
||||
$[11] = t2;
|
||||
} else {
|
||||
t2 = $[11];
|
||||
}
|
||||
const truncatedDesc = t2;
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`;
|
||||
const maxDescLength = Math.max(0, availableWidth)
|
||||
const truncatedDesc = truncateToWidth(
|
||||
item.description.replace(/\s+/g, ' '),
|
||||
maxDescLength,
|
||||
)
|
||||
lineContent = `${icon} ${displayText} – ${truncatedDesc}`
|
||||
} else {
|
||||
lineContent = `${icon} ${displayText}`;
|
||||
lineContent = `${icon} ${displayText}`
|
||||
}
|
||||
let t2;
|
||||
if ($[12] !== dimColor || $[13] !== lineContent || $[14] !== textColor) {
|
||||
t2 = <Text color={textColor} dimColor={dimColor} wrap="truncate">{lineContent}</Text>;
|
||||
$[12] = dimColor;
|
||||
$[13] = lineContent;
|
||||
$[14] = textColor;
|
||||
$[15] = t2;
|
||||
} else {
|
||||
t2 = $[15];
|
||||
}
|
||||
return t2;
|
||||
|
||||
return (
|
||||
<Text color={textColor} dimColor={dimColor} wrap="truncate">
|
||||
{lineContent}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
const maxNameWidth = Math.floor(columns * 0.4);
|
||||
const displayTextWidth = Math.min(maxColumnWidth ?? stringWidth(item.displayText) + 5, maxNameWidth);
|
||||
const textColor_0 = item.color || (isSelected ? "suggestion" : undefined);
|
||||
const shouldDim = !isSelected;
|
||||
let displayText_0 = item.displayText;
|
||||
if (stringWidth(displayText_0) > displayTextWidth - 2) {
|
||||
const t1 = displayTextWidth - 2;
|
||||
let t2;
|
||||
if ($[16] !== displayText_0 || $[17] !== t1) {
|
||||
t2 = truncateToWidth(displayText_0, t1);
|
||||
$[16] = displayText_0;
|
||||
$[17] = t1;
|
||||
$[18] = t2;
|
||||
} else {
|
||||
t2 = $[18];
|
||||
}
|
||||
displayText_0 = t2;
|
||||
|
||||
// For non-unified suggestions (commands, shell, etc.), use improved layout from main
|
||||
// Cap the command name column at 40% of terminal width to ensure description has space
|
||||
const maxNameWidth = Math.floor(columns * 0.4)
|
||||
const displayTextWidth = Math.min(
|
||||
maxColumnWidth ?? stringWidth(item.displayText) + 5,
|
||||
maxNameWidth,
|
||||
)
|
||||
|
||||
const textColor = item.color || (isSelected ? 'suggestion' : undefined)
|
||||
const shouldDim = !isSelected
|
||||
|
||||
// Truncate and pad the display text to fixed width
|
||||
let displayText = item.displayText
|
||||
if (stringWidth(displayText) > displayTextWidth - 2) {
|
||||
displayText = truncateToWidth(displayText, displayTextWidth - 2)
|
||||
}
|
||||
const paddedDisplayText = displayText_0 + " ".repeat(Math.max(0, displayTextWidth - stringWidth(displayText_0)));
|
||||
const tagText = item.tag ? `[${item.tag}] ` : "";
|
||||
const tagWidth = stringWidth(tagText);
|
||||
const descriptionWidth = Math.max(0, columns - displayTextWidth - tagWidth - 4);
|
||||
let t1;
|
||||
if ($[19] !== descriptionWidth || $[20] !== item.description) {
|
||||
t1 = item.description ? truncateToWidth(item.description.replace(/\s+/g, " "), descriptionWidth) : "";
|
||||
$[19] = descriptionWidth;
|
||||
$[20] = item.description;
|
||||
$[21] = t1;
|
||||
} else {
|
||||
t1 = $[21];
|
||||
}
|
||||
const truncatedDescription = t1;
|
||||
let t2;
|
||||
if ($[22] !== paddedDisplayText || $[23] !== shouldDim || $[24] !== textColor_0) {
|
||||
t2 = <Text color={textColor_0} dimColor={shouldDim}>{paddedDisplayText}</Text>;
|
||||
$[22] = paddedDisplayText;
|
||||
$[23] = shouldDim;
|
||||
$[24] = textColor_0;
|
||||
$[25] = t2;
|
||||
} else {
|
||||
t2 = $[25];
|
||||
}
|
||||
let t3;
|
||||
if ($[26] !== tagText) {
|
||||
t3 = tagText ? <Text dimColor={true}>{tagText}</Text> : null;
|
||||
$[26] = tagText;
|
||||
$[27] = t3;
|
||||
} else {
|
||||
t3 = $[27];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = !isSelected;
|
||||
let t6;
|
||||
if ($[28] !== t4 || $[29] !== t5 || $[30] !== truncatedDescription) {
|
||||
t6 = <Text color={t4} dimColor={t5}>{truncatedDescription}</Text>;
|
||||
$[28] = t4;
|
||||
$[29] = t5;
|
||||
$[30] = truncatedDescription;
|
||||
$[31] = t6;
|
||||
} else {
|
||||
t6 = $[31];
|
||||
}
|
||||
let t7;
|
||||
if ($[32] !== t2 || $[33] !== t3 || $[34] !== t6) {
|
||||
t7 = <Text wrap="truncate">{t2}{t3}{t6}</Text>;
|
||||
$[32] = t2;
|
||||
$[33] = t3;
|
||||
$[34] = t6;
|
||||
$[35] = t7;
|
||||
} else {
|
||||
t7 = $[35];
|
||||
}
|
||||
return t7;
|
||||
});
|
||||
const paddedDisplayText =
|
||||
displayText +
|
||||
' '.repeat(Math.max(0, displayTextWidth - stringWidth(displayText)))
|
||||
|
||||
const tagText = item.tag ? `[${item.tag}] ` : ''
|
||||
const tagWidth = stringWidth(tagText)
|
||||
const descriptionWidth = Math.max(
|
||||
0,
|
||||
columns - displayTextWidth - tagWidth - 4,
|
||||
)
|
||||
// Skill descriptions can contain newlines (e.g. /claude-api's "TRIGGER
|
||||
// when:" block). A multi-line row grows the overlay past minHeight; when
|
||||
// the filter narrows past that skill, the overlay shrinks and leaves
|
||||
// ghost rows. Flatten to one line before truncating.
|
||||
const truncatedDescription = item.description
|
||||
? truncateToWidth(item.description.replace(/\s+/g, ' '), descriptionWidth)
|
||||
: ''
|
||||
|
||||
return (
|
||||
<Text wrap="truncate">
|
||||
<Text color={textColor} dimColor={shouldDim}>
|
||||
{paddedDisplayText}
|
||||
</Text>
|
||||
{tagText ? <Text dimColor>{tagText}</Text> : null}
|
||||
<Text
|
||||
color={isSelected ? 'suggestion' : undefined}
|
||||
dimColor={!isSelected}
|
||||
>
|
||||
{truncatedDescription}
|
||||
</Text>
|
||||
</Text>
|
||||
)
|
||||
})
|
||||
|
||||
type Props = {
|
||||
suggestions: SuggestionItem[];
|
||||
selectedSuggestion: number;
|
||||
maxColumnWidth?: number;
|
||||
suggestions: SuggestionItem[]
|
||||
selectedSuggestion: number
|
||||
maxColumnWidth?: number
|
||||
/**
|
||||
* When true, the suggestions are rendered inside a position=absolute
|
||||
* overlay. We omit minHeight and flex-end so the y-clamp in the
|
||||
* renderer doesn't push fewer items down into the prompt area.
|
||||
*/
|
||||
overlay?: boolean;
|
||||
};
|
||||
export function PromptInputFooterSuggestions(t0) {
|
||||
const $ = _c(22);
|
||||
const {
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay
|
||||
} = t0;
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const maxVisibleItems = overlay ? OVERLAY_MAX_ITEMS : Math.min(6, Math.max(1, rows - 3));
|
||||
overlay?: boolean
|
||||
}
|
||||
|
||||
export function PromptInputFooterSuggestions({
|
||||
suggestions,
|
||||
selectedSuggestion,
|
||||
maxColumnWidth: maxColumnWidthProp,
|
||||
overlay,
|
||||
}: Props): ReactNode {
|
||||
const { rows } = useTerminalSize()
|
||||
// Maximum number of suggestions to show at once (leaving space for prompt).
|
||||
// Overlay mode (fullscreen) uses a fixed 5 — the floating box sits over
|
||||
// the ScrollBox, so terminal height isn't the constraint.
|
||||
const maxVisibleItems = overlay
|
||||
? OVERLAY_MAX_ITEMS
|
||||
: Math.min(6, Math.max(1, rows - 3))
|
||||
|
||||
// No suggestions to display
|
||||
if (suggestions.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t1;
|
||||
if ($[0] !== maxColumnWidthProp || $[1] !== suggestions) {
|
||||
t1 = maxColumnWidthProp ?? Math.max(...suggestions.map(_temp)) + 5;
|
||||
$[0] = maxColumnWidthProp;
|
||||
$[1] = suggestions;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
const maxColumnWidth = t1;
|
||||
const startIndex = Math.max(0, Math.min(selectedSuggestion - Math.floor(maxVisibleItems / 2), suggestions.length - maxVisibleItems));
|
||||
const endIndex = Math.min(startIndex + maxVisibleItems, suggestions.length);
|
||||
let T0;
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== endIndex || $[4] !== maxColumnWidth || $[5] !== overlay || $[6] !== selectedSuggestion || $[7] !== startIndex || $[8] !== suggestions) {
|
||||
const visibleItems = suggestions.slice(startIndex, endIndex);
|
||||
T0 = Box;
|
||||
t2 = "column";
|
||||
t3 = overlay ? undefined : "flex-end";
|
||||
let t5;
|
||||
if ($[13] !== maxColumnWidth || $[14] !== selectedSuggestion || $[15] !== suggestions) {
|
||||
t5 = item_0 => <SuggestionItemRow key={item_0.id} item={item_0} maxColumnWidth={maxColumnWidth} isSelected={item_0.id === suggestions[selectedSuggestion]?.id} />;
|
||||
$[13] = maxColumnWidth;
|
||||
$[14] = selectedSuggestion;
|
||||
$[15] = suggestions;
|
||||
$[16] = t5;
|
||||
} else {
|
||||
t5 = $[16];
|
||||
}
|
||||
t4 = visibleItems.map(t5);
|
||||
$[3] = endIndex;
|
||||
$[4] = maxColumnWidth;
|
||||
$[5] = overlay;
|
||||
$[6] = selectedSuggestion;
|
||||
$[7] = startIndex;
|
||||
$[8] = suggestions;
|
||||
$[9] = T0;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
T0 = $[9];
|
||||
t2 = $[10];
|
||||
t3 = $[11];
|
||||
t4 = $[12];
|
||||
}
|
||||
let t5;
|
||||
if ($[17] !== T0 || $[18] !== t2 || $[19] !== t3 || $[20] !== t4) {
|
||||
t5 = <T0 flexDirection={t2} justifyContent={t3}>{t4}</T0>;
|
||||
$[17] = T0;
|
||||
$[18] = t2;
|
||||
$[19] = t3;
|
||||
$[20] = t4;
|
||||
$[21] = t5;
|
||||
} else {
|
||||
t5 = $[21];
|
||||
}
|
||||
return t5;
|
||||
|
||||
// Use prop if provided (stable width from all commands), otherwise calculate from visible
|
||||
const maxColumnWidth =
|
||||
maxColumnWidthProp ??
|
||||
Math.max(...suggestions.map(item => stringWidth(item.displayText))) + 5
|
||||
|
||||
// Calculate visible items range based on selected index
|
||||
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)
|
||||
|
||||
// In non-overlay (inline) mode, justifyContent keeps suggestions
|
||||
// anchored to the bottom (near the prompt). In overlay mode we omit
|
||||
// both minHeight and flex-end: the parent is position=absolute with
|
||||
// bottom='100%', so its y is clamped to 0 by the renderer when it
|
||||
// would go negative. Adding minHeight + flex-end would create empty
|
||||
// padding rows that shift the visible items down into the prompt area
|
||||
// when the list has fewer items than maxVisibleItems.
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
justifyContent={overlay ? undefined : 'flex-end'}
|
||||
>
|
||||
{visibleItems.map(item => (
|
||||
<SuggestionItemRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
maxColumnWidth={maxColumnWidth}
|
||||
isSelected={item.id === suggestions[selectedSuggestion]?.id}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp(item) {
|
||||
return stringWidth(item.displayText);
|
||||
}
|
||||
export default memo(PromptInputFooterSuggestions);
|
||||
|
||||
export default memo(PromptInputFooterSuggestions)
|
||||
|
||||
Reference in New Issue
Block a user