fix: improve Windows UX and stabilize chat/session behavior across runtimes (#693)
* fix: preserve unsent prompt when adding editor context in VS Code * fix: append Add to chat selections as markdown blocks with stable spacing Convert selected assistant content to markdown before appending Wrap each Add to chat selection in an `md` fenced block Preserve multiline composer formatting across repeated appends * fix: normalize persisted Windows paths to prevent identity mismatches * fix: hide Windows subprocess console popups across server tasks Hide OpenCode startup and shell command child windows in the web server Apply windowsHide to cloudflared and skills-catalog git subprocesses Cover remaining git service exec paths that could surface console windows * fix: restore chat auto re-pin when reaching bottom Re-pin now triggers when scrolling back into the bottom zone, not only via the button. Upward user scroll intent still unpins immediately and is not overridden by re-pin. Unified bottom/re-pin threshold logic to reduce sensitivity mismatches. * fix: restore chat scroll release on mobile during streaming Restores pinned-scroll release on touch scroll up so mobile users can leave auto-follow while streaming. Improves re-pin behavior near bottom to avoid sticky or inconsistent pin states. Includes related chat UI and dependency updates in the same change set. * fix: hide daemon startup probe consoles on Windows * fix: prevent pinned scroll tug-of-war during streaming * fix: prefer git.exe to avoid Windows diff popup flashes * fix: prefer git.exe discovery in Windows git flows * fix: avoid where probes in Windows git resolution * fix: avoid update-check subprocess flashes on Windows * fix: normalize read file path labels * feat: add OpenChamber defaults and improve theme ports Add new OpenChamber light and dark themes Regenerate imported themes with stronger surface mapping Set OpenChamber themes as the default top options * fix: stabilize chat pin and unpin behavior during streaming Restores reliable unpin on upward wheel and touch gestures while auto-follow is active. Prevents immediate re-pin while the user is actively scrolling upward near the bottom. Keeps smooth follow-to-bottom behavior while reducing scroll tug-of-war. * fix: suppress Windows command popups in VSCode runtime processes Hide spawned git and server process windows in VS Code runtime Extend hidden-window handling to server port cleanup and reveal commands Keep behavior unchanged on non-Windows platforms
This commit is contained in:
committed by
GitHub
parent
a07c068b66
commit
3123de5f43
@@ -59,6 +59,24 @@ const MAX_VISIBLE_TEXTAREA_LINES = 8;
|
||||
const EMPTY_QUEUE: QueuedMessage[] = [];
|
||||
const FILE_MENTION_TOKEN = /^@[^\s]+$/;
|
||||
|
||||
const appendWithLineBreaks = (base: string, next: string): string => {
|
||||
const separator = !base
|
||||
? ''
|
||||
: base.endsWith('\n\n')
|
||||
? ''
|
||||
: base.endsWith('\n')
|
||||
? '\n'
|
||||
: '\n\n';
|
||||
|
||||
const nextWithTrailingBreaks = next.endsWith('\n\n')
|
||||
? next
|
||||
: next.endsWith('\n')
|
||||
? `${next}\n`
|
||||
: `${next}\n\n`;
|
||||
|
||||
return `${base}${separator}${nextWithTrailingBreaks}`;
|
||||
};
|
||||
|
||||
interface ChatInputProps {
|
||||
onOpenSettings?: () => void;
|
||||
scrollToBottom?: (options?: { instant?: boolean; force?: boolean }) => void;
|
||||
@@ -589,9 +607,9 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
||||
if (pending?.text) {
|
||||
if (pending.mode === 'append') {
|
||||
setMessage((prev) => {
|
||||
if (!pending.text) return prev;
|
||||
if (!prev.trim()) return pending.text;
|
||||
return `${prev}\n\n${pending.text}`;
|
||||
const next = pending.text;
|
||||
if (!next.trim()) return prev;
|
||||
return appendWithLineBreaks(prev, next);
|
||||
});
|
||||
} else {
|
||||
setMessage(pending.text);
|
||||
|
||||
@@ -1097,14 +1097,25 @@ const MessageList = React.forwardRef<MessageListHandle, MessageListProps>(({
|
||||
useFlushSync: false,
|
||||
});
|
||||
|
||||
const virtualRows = shouldVirtualize ? virtualizer.getVirtualItems() : [];
|
||||
const isVirtualRowInRange = React.useCallback(
|
||||
(row: VirtualItem) => row.index >= 0 && row.index < stagedEntries.length,
|
||||
[stagedEntries.length],
|
||||
);
|
||||
|
||||
const virtualRows = shouldVirtualize ? virtualizer.getVirtualItems().filter(isVirtualRowInRange) : [];
|
||||
const lastNonEmptyVirtualRowsRef = React.useRef<VirtualItem[]>([]);
|
||||
if (shouldVirtualize && virtualRows.length > 0) {
|
||||
lastNonEmptyVirtualRowsRef.current = virtualRows;
|
||||
} else if (!shouldVirtualize && lastNonEmptyVirtualRowsRef.current.length > 0) {
|
||||
lastNonEmptyVirtualRowsRef.current = [];
|
||||
}
|
||||
|
||||
const fallbackVirtualRows = shouldVirtualize
|
||||
? lastNonEmptyVirtualRowsRef.current.filter(isVirtualRowInRange)
|
||||
: [];
|
||||
|
||||
const effectiveVirtualRows = shouldVirtualize
|
||||
? (virtualRows.length > 0 ? virtualRows : lastNonEmptyVirtualRowsRef.current)
|
||||
? (virtualRows.length > 0 ? virtualRows : fallbackVirtualRows)
|
||||
: [];
|
||||
|
||||
const renderVirtualized = shouldVirtualize && effectiveVirtualRows.length > 0;
|
||||
|
||||
@@ -16,17 +16,147 @@ interface MenuPosition {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
interface SelectionPayload {
|
||||
plainText: string;
|
||||
markdownText: string;
|
||||
rect: DOMRect;
|
||||
}
|
||||
|
||||
const DESKTOP_MENU_SIDE_MARGIN_PX = 8;
|
||||
const DESKTOP_MENU_FALLBACK_WIDTH_PX = 280;
|
||||
|
||||
const normalizeLineBreaks = (value: string): string => value.replace(/\r\n?/g, '\n');
|
||||
|
||||
const trimSelectionValue = (value: string): string => normalizeLineBreaks(value).trim();
|
||||
|
||||
const textToMarkdownInline = (value: string): string => value.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const renderInlineMarkdownNode = (node: Node): string => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return textToMarkdownInline(node.textContent || '');
|
||||
}
|
||||
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const element = node as HTMLElement;
|
||||
const tag = element.tagName.toLowerCase();
|
||||
const childText = Array.from(element.childNodes)
|
||||
.map((child) => renderInlineMarkdownNode(child))
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (!childText && tag !== 'br') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (tag === 'br') return '\n';
|
||||
if (tag === 'strong' || tag === 'b') return `**${childText}**`;
|
||||
if (tag === 'em' || tag === 'i') return `*${childText}*`;
|
||||
if (tag === 'code') return `\`${childText.replace(/`/g, '\\`')}\``;
|
||||
if (tag === 'a') {
|
||||
const href = element.getAttribute('href');
|
||||
return href ? `[${childText}](${href})` : childText;
|
||||
}
|
||||
|
||||
return childText;
|
||||
};
|
||||
|
||||
const renderListMarkdown = (list: HTMLElement, ordered: boolean): string => {
|
||||
const items = Array.from(list.children).filter(
|
||||
(child): child is HTMLElement => child instanceof HTMLElement && child.tagName.toLowerCase() === 'li'
|
||||
);
|
||||
|
||||
return items
|
||||
.map((item, index) => {
|
||||
const prefix = ordered ? `${index + 1}. ` : '- ';
|
||||
const body = Array.from(item.childNodes)
|
||||
.map((child) => renderInlineMarkdownNode(child))
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
return body ? `${prefix}${body}` : '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const renderBlockMarkdownNode = (node: Node): string => {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return trimSelectionValue(node.textContent || '');
|
||||
}
|
||||
|
||||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const element = node as HTMLElement;
|
||||
const tag = element.tagName.toLowerCase();
|
||||
|
||||
if (tag === 'pre') {
|
||||
const codeElement = element.querySelector('code');
|
||||
const languageClass = codeElement?.className || '';
|
||||
const language = (languageClass.match(/language-([\w-]+)/)?.[1] || '').trim();
|
||||
const code = normalizeLineBreaks(codeElement?.textContent || element.textContent || '').replace(/\n$/, '');
|
||||
return `\`\`\`${language}\n${code}\n\`\`\``;
|
||||
}
|
||||
|
||||
if (tag === 'ul') return renderListMarkdown(element, false);
|
||||
if (tag === 'ol') return renderListMarkdown(element, true);
|
||||
|
||||
if (tag === 'blockquote') {
|
||||
const content = trimSelectionValue(
|
||||
Array.from(element.childNodes).map((child) => renderBlockMarkdownNode(child)).join('\n')
|
||||
);
|
||||
return content
|
||||
.split('\n')
|
||||
.filter((line) => line.length > 0)
|
||||
.map((line) => `> ${line}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
if (/^h[1-6]$/.test(tag)) {
|
||||
const level = Number.parseInt(tag[1], 10);
|
||||
const text = trimSelectionValue(Array.from(element.childNodes).map((child) => renderInlineMarkdownNode(child)).join(''));
|
||||
return text ? `${'#'.repeat(level)} ${text}` : '';
|
||||
}
|
||||
|
||||
if (tag === 'p' || tag === 'div' || tag === 'li') {
|
||||
return trimSelectionValue(Array.from(element.childNodes).map((child) => renderInlineMarkdownNode(child)).join(''));
|
||||
}
|
||||
|
||||
const blockChildren = Array.from(element.childNodes)
|
||||
.map((child) => renderBlockMarkdownNode(child))
|
||||
.filter((child) => child.length > 0);
|
||||
if (blockChildren.length > 0) {
|
||||
return blockChildren.join('\n\n');
|
||||
}
|
||||
|
||||
return trimSelectionValue(Array.from(element.childNodes).map((child) => renderInlineMarkdownNode(child)).join(''));
|
||||
};
|
||||
|
||||
const rangeToMarkdown = (range: Range, plainText: string): string => {
|
||||
const fragment = range.cloneContents();
|
||||
const markdown = Array.from(fragment.childNodes)
|
||||
.map((node) => renderBlockMarkdownNode(node))
|
||||
.filter((value) => value.length > 0)
|
||||
.join('\n\n')
|
||||
.trim();
|
||||
|
||||
return markdown || trimSelectionValue(plainText);
|
||||
};
|
||||
|
||||
export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerRef }) => {
|
||||
const [position, setPosition] = React.useState<MenuPosition>({ x: 0, y: 0, show: false });
|
||||
const [selectedText, setSelectedText] = React.useState('');
|
||||
const [selectedTextMarkdown, setSelectedTextMarkdown] = React.useState('');
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
const [isOpening, setIsOpening] = React.useState(false);
|
||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||
const menuWidthRef = React.useRef(DESKTOP_MENU_FALLBACK_WIDTH_PX);
|
||||
const pendingSelectionRef = React.useRef<{ text: string; rect: DOMRect } | null>(null);
|
||||
const pendingSelectionRef = React.useRef<SelectionPayload | null>(null);
|
||||
const openRafRef = React.useRef<number | null>(null);
|
||||
const isMenuVisibleRef = React.useRef(false);
|
||||
const createSession = useSessionStore((state) => state.createSession);
|
||||
@@ -61,6 +191,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
|
||||
setPosition((prev) => ({ ...prev, show: false }));
|
||||
setSelectedText('');
|
||||
setSelectedTextMarkdown('');
|
||||
isMenuVisibleRef.current = false;
|
||||
}, []);
|
||||
|
||||
@@ -85,7 +216,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
const showMenu = React.useCallback(() => {
|
||||
if (!pendingSelectionRef.current) return;
|
||||
|
||||
const { text, rect } = pendingSelectionRef.current;
|
||||
const { plainText, markdownText, rect } = pendingSelectionRef.current;
|
||||
const shouldAnimateIn = !position.show;
|
||||
|
||||
// Position menu above the selection
|
||||
@@ -94,7 +225,8 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
: getDesktopClampedX(rect.left + rect.width / 2);
|
||||
const menuY = rect.top - 10;
|
||||
|
||||
setSelectedText(text);
|
||||
setSelectedText(plainText);
|
||||
setSelectedTextMarkdown(markdownText);
|
||||
setPosition({
|
||||
x: menuX,
|
||||
y: menuY,
|
||||
@@ -160,7 +292,7 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
return;
|
||||
}
|
||||
|
||||
const text = selection.toString().trim();
|
||||
const text = trimSelectionValue(selection.toString());
|
||||
|
||||
// Only show if we have text and the selection is within our container
|
||||
if (!text) {
|
||||
@@ -184,7 +316,11 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
// Store the selection but don't show menu yet if dragging
|
||||
pendingSelectionRef.current = { text, rect };
|
||||
pendingSelectionRef.current = {
|
||||
plainText: text,
|
||||
markdownText: rangeToMarkdown(range, text),
|
||||
rect,
|
||||
};
|
||||
|
||||
// Only show menu if we're not currently dragging
|
||||
if (!isDragging) {
|
||||
@@ -247,16 +383,16 @@ export const TextSelectionMenu: React.FC<TextSelectionMenuProps> = ({ containerR
|
||||
}, [containerRef, handleSelectionChange, hideMenu, showMenu]);
|
||||
|
||||
const handleAddToChat = React.useCallback(() => {
|
||||
if (!selectedText) return;
|
||||
if (!selectedTextMarkdown) return;
|
||||
|
||||
const fenced = `\`\`\`md\n${selectedText}\n\`\`\``;
|
||||
setPendingInputText(fenced, 'append');
|
||||
const markdownBlock = `\`\`\`md\n${selectedTextMarkdown}\n\`\`\``;
|
||||
setPendingInputText(markdownBlock, 'append');
|
||||
|
||||
hideMenu();
|
||||
|
||||
// Clear selection
|
||||
window.getSelection()?.removeAllRanges();
|
||||
}, [selectedText, setPendingInputText, hideMenu]);
|
||||
}, [selectedTextMarkdown, setPendingInputText, hideMenu]);
|
||||
|
||||
const handleCreateNewSession = React.useCallback(async () => {
|
||||
if (!selectedText) return;
|
||||
|
||||
@@ -206,7 +206,7 @@ const normalizePathValue = (value: string): string => {
|
||||
if (!trimmed) {
|
||||
return '';
|
||||
}
|
||||
return trimmed.replace(/\\/g, '/');
|
||||
return trimmed.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||||
};
|
||||
|
||||
const trimTrailingSlashes = (value: string): string => {
|
||||
@@ -240,6 +240,45 @@ const getRelativePathFromDirectory = (filePath: string, currentDirectory: string
|
||||
return normalizedPath;
|
||||
};
|
||||
|
||||
const renderReadFilePath = (displayPath: string) => {
|
||||
const lastSlash = displayPath.lastIndexOf('/');
|
||||
|
||||
if (lastSlash === -1) {
|
||||
return (
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-title)' }}
|
||||
title={displayPath}
|
||||
>
|
||||
{displayPath}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const dir = displayPath.slice(0, lastSlash);
|
||||
const name = displayPath.slice(lastSlash + 1);
|
||||
const hasAbsoluteRoot = dir.startsWith('/');
|
||||
const displayDir = hasAbsoluteRoot ? dir.slice(1) : dir;
|
||||
|
||||
return (
|
||||
<span className="min-w-0 inline-flex max-w-full flex-1 items-baseline overflow-hidden typography-meta leading-5" title={displayPath}>
|
||||
{hasAbsoluteRoot ? <span className="flex-shrink-0" style={{ color: 'var(--tools-description)' }}>/</span> : null}
|
||||
<span
|
||||
className="min-w-0 shrink truncate whitespace-nowrap"
|
||||
style={{
|
||||
color: 'var(--tools-description)',
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{displayDir}
|
||||
</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-description)' }}>/</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-title)' }}>{name}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const resolveAbsolutePath = (currentDirectory: string, filePath: string): string => {
|
||||
const normalizedPath = normalizePathValue(filePath);
|
||||
if (!normalizedPath) {
|
||||
@@ -502,17 +541,7 @@ export const StaticToolRow: React.FC<{
|
||||
title={entry.offset ? `${entry.displayPath}:${entry.offset}` : entry.displayPath}
|
||||
>
|
||||
{showToolFileIcons ? <FileTypeIcon filePath={entry.path} className="h-3.5 w-3.5" /> : null}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{
|
||||
color: 'var(--tools-description)',
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
title={entry.displayPath}
|
||||
>
|
||||
{entry.displayPath}
|
||||
</span>
|
||||
{renderReadFilePath(entry.displayPath)}
|
||||
</button>
|
||||
))
|
||||
: null}
|
||||
@@ -520,7 +549,7 @@ export const StaticToolRow: React.FC<{
|
||||
? descriptions.map((desc, index) => (
|
||||
<span key={`${desc}-${index}`} className="inline-flex min-w-0 flex-1">
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
variant={animateTailText ? 'generate-effect' : 'static'}
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
title={desc}
|
||||
@@ -550,7 +579,7 @@ export const StaticToolRow: React.FC<{
|
||||
: null}
|
||||
{!isReadGroup && !isSearchGroup && !isFetchGroup && descriptions.length > 0 ? (
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
variant={animateTailText ? 'generate-effect' : 'static'}
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
>
|
||||
|
||||
@@ -103,7 +103,7 @@ const getMultiFileDescription = (
|
||||
<span key={entry.path} className="inline-flex min-w-0 max-w-full items-center gap-1 typography-meta leading-5" style={{ color: 'var(--tools-description)' }}>
|
||||
{showFileIcons ? <FileTypeIcon filePath={entry.path} className="h-3.5 w-3.5" /> : null}
|
||||
<Text
|
||||
variant={animate ? 'generate-effect' : undefined}
|
||||
variant={animate ? 'generate-effect' : 'static'}
|
||||
className="min-w-0 max-w-full truncate typography-meta leading-5"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
title={entry.path}
|
||||
@@ -302,14 +302,36 @@ const getPrimaryDiffFromMetadata = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getRelativePath = (absolutePath: string, currentDirectory: string): string => {
|
||||
if (absolutePath.startsWith(currentDirectory)) {
|
||||
const relativePath = absolutePath.substring(currentDirectory.length);
|
||||
const normalizeDisplayPath = (value: string): string => {
|
||||
const trimmed = value.trim().replace(/\\/g, '/').replace(/\/{2,}/g, '/');
|
||||
if (!trimmed || trimmed === '/') {
|
||||
return trimmed;
|
||||
}
|
||||
return trimmed.replace(/\/+$/, '');
|
||||
};
|
||||
|
||||
return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath;
|
||||
const getRelativePath = (absolutePath: string, currentDirectory: string): string => {
|
||||
const normalizedAbsolutePath = normalizeDisplayPath(absolutePath);
|
||||
const normalizedCurrentDirectory = normalizeDisplayPath(currentDirectory);
|
||||
|
||||
if (!normalizedAbsolutePath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return absolutePath;
|
||||
if (!normalizedCurrentDirectory) {
|
||||
return normalizedAbsolutePath;
|
||||
}
|
||||
|
||||
if (normalizedAbsolutePath === normalizedCurrentDirectory) {
|
||||
return '.';
|
||||
}
|
||||
|
||||
const prefix = `${normalizedCurrentDirectory}/`;
|
||||
if (normalizedAbsolutePath.startsWith(prefix)) {
|
||||
return normalizedAbsolutePath.slice(prefix.length);
|
||||
}
|
||||
|
||||
return normalizedAbsolutePath;
|
||||
};
|
||||
|
||||
const usePierreThemeConfig = () => {
|
||||
@@ -390,6 +412,13 @@ const getToolDescriptionPath = (part: ToolPartType, state: ToolStateUnion, curre
|
||||
}
|
||||
}
|
||||
|
||||
if (part.tool === 'read' && input) {
|
||||
const filePath = input?.filePath || input?.file_path || input?.path || metadata?.filePath || metadata?.file_path || metadata?.path;
|
||||
if (typeof filePath === 'string') {
|
||||
return getRelativePath(filePath, currentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
if (['write', 'create', 'file_write'].includes(part.tool) && input) {
|
||||
const filePath = input?.filePath || input?.file_path || input?.path;
|
||||
if (typeof filePath === 'string') {
|
||||
@@ -853,7 +882,7 @@ const TaskToolSummary: React.FC<{
|
||||
</span>
|
||||
) : (
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
variant={animateTailText ? 'generate-effect' : 'static'}
|
||||
className={cn(
|
||||
'typography-meta flex-1 min-w-0 text-muted-foreground/70',
|
||||
isMobile ? 'whitespace-normal break-words' : 'truncate'
|
||||
@@ -964,11 +993,14 @@ const renderPathLikeGitChanges = (path: string, grow = true) => {
|
||||
|
||||
const dir = path.slice(0, lastSlash);
|
||||
const name = path.slice(lastSlash + 1);
|
||||
const hasAbsoluteRoot = dir.startsWith('/');
|
||||
const displayDir = hasAbsoluteRoot ? dir.slice(1) : dir;
|
||||
|
||||
return (
|
||||
<span className={cn('min-w-0 flex items-baseline overflow-hidden typography-ui-label', grow && 'flex-1')} title={path}>
|
||||
{hasAbsoluteRoot ? <span className="flex-shrink-0 text-muted-foreground">/</span> : null}
|
||||
<span className="min-w-0 truncate text-muted-foreground" style={{ direction: 'rtl', textAlign: 'left' }}>
|
||||
{dir}
|
||||
{displayDir}
|
||||
</span>
|
||||
<span className="flex-shrink-0">
|
||||
<span className="text-muted-foreground">/</span>
|
||||
@@ -998,20 +1030,23 @@ const renderAnimatedPathWithIcon = (path: string, _animate = true, grow = true,
|
||||
|
||||
const dir = path.slice(0, lastSlash);
|
||||
const name = path.slice(lastSlash + 1);
|
||||
const hasAbsoluteRoot = dir.startsWith('/');
|
||||
const displayDir = hasAbsoluteRoot ? dir.slice(1) : dir;
|
||||
|
||||
return (
|
||||
<span className={cn('min-w-0 inline-flex items-center gap-1 overflow-hidden', grow && 'flex-1')} title={path}>
|
||||
{showFileIcons ? <FileTypeIcon filePath={path} className="h-3.5 w-3.5 flex-shrink-0" /> : null}
|
||||
<span className={cn('min-w-0 inline-flex items-baseline overflow-hidden typography-meta', grow && 'flex-1')}>
|
||||
<span className={cn('min-w-0 inline-flex max-w-full items-baseline overflow-hidden typography-meta', grow && 'flex-1')}>
|
||||
{hasAbsoluteRoot ? <span className="flex-shrink-0" style={{ color: 'var(--tools-description)' }}>/</span> : null}
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate whitespace-nowrap"
|
||||
className="min-w-0 shrink truncate whitespace-nowrap"
|
||||
style={{
|
||||
color: 'var(--tools-description)',
|
||||
direction: 'rtl',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{dir}
|
||||
{displayDir}
|
||||
</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-description)' }}>/</span>
|
||||
<span className="flex-shrink-0" style={{ color: 'var(--tools-title)' }}>
|
||||
@@ -1933,7 +1968,7 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
renderAnimatedPathWithIcon(descriptionPath, animateTailText, false, showToolFileIcons)
|
||||
) : (
|
||||
<Text
|
||||
variant={animateTailText ? 'generate-effect' : undefined}
|
||||
variant={animateTailText ? 'generate-effect' : 'static'}
|
||||
className="min-w-0 truncate typography-meta"
|
||||
style={{ color: 'var(--tools-description)' }}
|
||||
title={description}
|
||||
|
||||
@@ -127,6 +127,12 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
|
||||
const isRepo = props.projectRepoStatus.get(projectKey);
|
||||
const isHovered = props.hoveredProjectId === projectKey;
|
||||
const orderedGroups = props.getOrderedGroups(projectKey, section.groups);
|
||||
const sortableEntries = orderedGroups.map((group) => ({
|
||||
sortableId: `${projectKey}:${group.id}`,
|
||||
groupId: group.id,
|
||||
}));
|
||||
const sortableGroupIds = sortableEntries.map((entry) => entry.sortableId);
|
||||
const sortableIdToGroupId = new Map(sortableEntries.map((entry) => [entry.sortableId, entry.groupId]));
|
||||
|
||||
return (
|
||||
<SortableProjectItem
|
||||
@@ -184,8 +190,11 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
|
||||
onDragEnd={(event) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
const oldIndex = orderedGroups.findIndex((item) => item.id === active.id);
|
||||
const newIndex = orderedGroups.findIndex((item) => item.id === over.id);
|
||||
const activeId = typeof active.id === 'string' ? sortableIdToGroupId.get(active.id) : null;
|
||||
const overId = typeof over.id === 'string' ? sortableIdToGroupId.get(over.id) : null;
|
||||
if (!activeId || !overId) return;
|
||||
const oldIndex = orderedGroups.findIndex((item) => item.id === activeId);
|
||||
const newIndex = orderedGroups.findIndex((item) => item.id === overId);
|
||||
if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return;
|
||||
const next = arrayMove(orderedGroups, oldIndex, newIndex).map((item) => item.id);
|
||||
props.setGroupOrderByProject((prev) => {
|
||||
@@ -195,11 +204,11 @@ export function SidebarProjectsList(props: Props): React.ReactNode {
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SortableContext items={orderedGroups.map((group) => group.id)} strategy={verticalListSortingStrategy}>
|
||||
<SortableContext items={sortableGroupIds} strategy={verticalListSortingStrategy}>
|
||||
{orderedGroups.map((group) => {
|
||||
const groupKey = `${projectKey}:${group.id}`;
|
||||
return (
|
||||
<SortableGroupItem key={group.id} id={group.id} disabled={props.isInlineEditing}>
|
||||
<SortableGroupItem key={groupKey} id={groupKey} disabled={props.isInlineEditing}>
|
||||
{props.renderGroupSessions(group, groupKey, projectKey)}
|
||||
</SortableGroupItem>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,14 @@ type Variant = {
|
||||
};
|
||||
|
||||
const variants = [
|
||||
{
|
||||
variant: "static",
|
||||
component: ({ children, className, ...props }) => (
|
||||
<span {...props} className={className}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
variant: "shine",
|
||||
component: ({ children, className, ...props }) => (
|
||||
@@ -210,7 +218,7 @@ export type TextProps = {
|
||||
Partial<MotionProps>;
|
||||
|
||||
export function Text({ variant = "shine", className, ...props }: TextProps) {
|
||||
const FALLBACK_INDEX = 0;
|
||||
const FALLBACK_INDEX = 1;
|
||||
|
||||
const variantComponent = variants.find((v) => v.variant === variant)?.component;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user