Repository navigation
fix(markdown): preserve descriptive file-link labels #10794
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8cbfa10
2914b6a
d00e8ea
450620e
72b864e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,7 +50,10 @@ import { | |
| classifyMarkdownImageSource, | ||
| markdownImageSourceFragment, | ||
| } from "@t3tools/client-runtime/markdown-images"; | ||
| import { inlineCodeFilePathCandidate } from "@t3tools/client-runtime/markdown-links"; | ||
| import { | ||
| inlineCodeFilePathCandidate, | ||
| isMarkdownFileLinkLabel, | ||
| } from "@t3tools/client-runtime/markdown-links"; | ||
| import { mediaFileReference, mediaUrlReference } from "@t3tools/client-runtime/media-reference"; | ||
| import { mediaKindFromPath, mediaMimeTypeFromExtension } from "@t3tools/shared/filePreview"; | ||
| import * as Cause from "effect/Cause"; | ||
|
|
@@ -1185,6 +1188,7 @@ function UncachedShikiCodeBlock({ | |
| } | ||
|
|
||
| interface MarkdownFileLinkProps { | ||
| children?: ReactNode; | ||
| href: string; | ||
| targetPath: string; | ||
| iconPath: string; | ||
|
|
@@ -1926,6 +1930,7 @@ function MarkdownExternalLinkContent({ | |
| } | ||
|
|
||
| const MarkdownFileLink = memo(function MarkdownFileLink({ | ||
| children, | ||
| href, | ||
| targetPath, | ||
| iconPath, | ||
|
|
@@ -2207,45 +2212,62 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ | |
| canOpenInPanel, | ||
| }); | ||
|
|
||
| const chip = <FileTagChipContent path={iconPath} label={label} theme={theme} />; | ||
| const triggerClassName = cn(MARKDOWN_FILE_LINK_CLASS_NAME, !hasPrimaryAction && "select-text"); | ||
| const triggerProps = hasPrimaryAction | ||
| ? { | ||
| "data-markdown-copy": copyMarkdown, | ||
| onClick: (event: ReactMouseEvent<HTMLElement>) => { | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| if (onOpen && shouldOpenMarkdownFileLinkInEditor(event)) { | ||
| handleOpenInEditor(); | ||
| return; | ||
| } | ||
| if (useBrowserPrimaryAction) { | ||
| handleOpenInBrowser(); | ||
| return; | ||
| } | ||
| handleOpenInFilePreview(); | ||
| }, | ||
| onContextMenu: handleContextMenu, | ||
| } | ||
| : { | ||
| "aria-label": children ? undefined : `File options for ${label}`, | ||
| "aria-haspopup": "menu" as const, | ||
| "data-markdown-copy": copyMarkdown, | ||
| onClick: handleContextMenu, | ||
| onContextMenu: handleContextMenu, | ||
| }; | ||
| // A descriptive label stays link text; only the destination keeps the chip look. | ||
| const descriptiveContent = children ? ( | ||
| <> | ||
| {children} (<ContextChip kind="mention">{chip}</ContextChip>) | ||
| </> | ||
| ) : null; | ||
|
|
||
| return ( | ||
| <Tooltip> | ||
| <TooltipTrigger | ||
| render={ | ||
| hasPrimaryAction ? ( | ||
| <ContextChip | ||
| kind="mention" | ||
| render={<a href={href} />} | ||
| className={MARKDOWN_FILE_LINK_CLASS_NAME} | ||
| data-markdown-copy={copyMarkdown} | ||
| onClick={(event) => { | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| if (onOpen && shouldOpenMarkdownFileLinkInEditor(event)) { | ||
| handleOpenInEditor(); | ||
| return; | ||
| } | ||
| if (useBrowserPrimaryAction) { | ||
| handleOpenInBrowser(); | ||
| return; | ||
| } | ||
| handleOpenInFilePreview(); | ||
| }} | ||
| onContextMenu={handleContextMenu} | ||
| > | ||
| <FileTagChipContent path={iconPath} label={label} theme={theme} /> | ||
| </ContextChip> | ||
| descriptiveContent ? ( | ||
| hasPrimaryAction ? ( | ||
| <a href={href} className={cn(triggerClassName, "text-left")} {...triggerProps}> | ||
| {descriptiveContent} | ||
| </a> | ||
| ) : ( | ||
| <button type="button" className={cn(triggerClassName, "text-left")} {...triggerProps}> | ||
| {descriptiveContent} | ||
| </button> | ||
| ) | ||
| ) : ( | ||
| <ContextChip | ||
| kind="mention" | ||
| render={<button type="button" />} | ||
| aria-label={`File options for ${label}`} | ||
| aria-haspopup="menu" | ||
| className={cn(MARKDOWN_FILE_LINK_CLASS_NAME, "select-text")} | ||
| data-markdown-copy={copyMarkdown} | ||
| onClick={handleContextMenu} | ||
| onContextMenu={handleContextMenu} | ||
| render={hasPrimaryAction ? <a href={href} /> : <button type="button" />} | ||
| className={triggerClassName} | ||
| {...triggerProps} | ||
| > | ||
| <FileTagChipContent path={iconPath} label={label} theme={theme} /> | ||
| {chip} | ||
| </ContextChip> | ||
| ) | ||
| } | ||
|
|
@@ -2266,6 +2288,7 @@ function areMarkdownFileLinkPropsEqual( | |
| next: Readonly<MarkdownFileLinkProps>, | ||
| ): boolean { | ||
| return ( | ||
| previous.children === next.children && | ||
| previous.href === next.href && | ||
| previous.targetPath === next.targetPath && | ||
| previous.iconPath === next.iconPath && | ||
|
|
@@ -2611,7 +2634,12 @@ function useChatMarkdownState({ | |
| [cwd, findWorkspaceBasenameMatch, revealFileInFileManager], | ||
| ); | ||
| const fileLinkChip = useCallback( | ||
| (fileLinkMeta: MarkdownFileLinkMeta, copyMarkdown: string, mediaSource?: string) => { | ||
| ( | ||
| fileLinkMeta: MarkdownFileLinkMeta, | ||
| copyMarkdown: string, | ||
| mediaSource?: string, | ||
| children?: ReactNode, | ||
| ) => { | ||
| const parentSuffix = fileLinkParentSuffixByPath.get( | ||
| fileLinkMeta.filePath.replaceAll("\\", "/"), | ||
| ); | ||
|
|
@@ -2668,7 +2696,9 @@ function useChatMarkdownState({ | |
| ? () => openMarkdownFileInPreview(fileLinkMeta.filePath) | ||
| : undefined | ||
| } | ||
| /> | ||
| > | ||
| {children} | ||
| </MarkdownFileLink> | ||
| ); | ||
| }, | ||
| [ | ||
|
|
@@ -3101,10 +3131,17 @@ const CHAT_MARKDOWN_COMPONENTS = { | |
| ); | ||
| } | ||
|
|
||
| const label = nodeToPlainText(children); | ||
| const escapedLabel = (label.trim() ? label : fileLinkMeta.basename) | ||
| .replaceAll("\\", "\\\\") | ||
| .replaceAll("[", "\\[") | ||
| .replaceAll("]", "\\]"); | ||
| const copyMarkdown = `[${escapedLabel}](${normalizedHref})`; | ||
| return fileLinkChip( | ||
| fileLinkMeta, | ||
| `[${fileLinkMeta.basename}](${normalizedHref})`, | ||
| copyMarkdown, | ||
| normalizedHref, | ||
| isMarkdownFileLinkLabel(label, normalizedHref) ? undefined : children, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1930,2000p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2100,2298p' apps/web/src/components/ChatMarkdown.tsx
sed -n '3070,3225p' apps/web/src/components/ChatMarkdown.tsx
rg -n 'MarkdownCode|stopPropagation|Context.Provider|linkLabel' apps/web/src/components/ChatMarkdown.tsxRepository: pingdotgg/t3code Length of output: 14772 🏁 Script executed: rg -n "MarkdownLinkContext|ChatMarkdownRendererContext|fileLinkChip|isMarkdownFileLinkLabel|function MarkdownFileLink|onClick" apps/web/src/components/ChatMarkdown.tsx
sed -n '1,180p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2260,2440p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2920,3175p' apps/web/src/components/ChatMarkdown.tsx
rg -n "MarkdownCode|dataInlineCode|MarkdownLinkContext|fileLinkChip" apps/web/src --glob '*.tsx' --glob '*.ts'Repository: pingdotgg/t3code Length of output: 27837 🏁 Script executed: sed -n '1375,1420p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2198,2250p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2618,2770p' apps/web/src/components/ChatMarkdown.tsx
sed -n '2785,2815p' apps/web/src/components/ChatMarkdown.tsx
sed -n '3375,3420p' apps/web/src/components/ChatMarkdown.tsxRepository: pingdotgg/t3code Length of output: 10913 Keep inline paths inside descriptive labels as code text.
Wrap retained children in Suggested fix const descriptiveContent = children ? (
<>
- {children} (<ContextChip kind="mention">{chip}</ContextChip>)
+ <MarkdownLinkContext value>{children}</MarkdownLinkContext>{" "}
+ (<ContextChip kind="mention">{chip}</ContextChip>)
</>
) : null; code: function MarkdownCode({ node, children, className, ...props }) {
+ const isLinkLabel = use(MarkdownLinkContext);
const { cwd, imageBaseDir, inlineCodeFileLinkMetaByText, fileLinkChip } = use(
ChatMarkdownRendererContext,
);
- if (node?.properties?.dataInlineCode != null) {
+ if (!isLinkLabel && node?.properties?.dataInlineCode != null) {🤖 Prompt for AI Agents |
||
| ); | ||
| }, | ||
| code: function MarkdownCode({ node, children, className, ...props }) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
src/nativeMarkdownText.ts:488On Android, copying a descriptive file link drops its prose label from the canonical Markdown link, producing text such as
validates the input [(example.ts)](<...>)instead of a link labeledvalidates the input.appendChildrenemits the label as an ordinaryhrefrun, while line 488 emits only the filename suffix as thefileIconrun thatnativeMarkdownContextCopyRangescanonicalizes. Keep the descriptive label and filename in the same canonicalized file-link run, or update the copy-range logic to include the preceding label run.🤖 Copy this AI Prompt to have your agent fix this: