diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 27a38261d..5219b79ad 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -68,7 +68,7 @@ import type { AgentAttachment } from "@server/shared/messages"; import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types"; import { buildToolCallPresentation } from "@/tool-calls/presentation"; import { resolveToolCallIcon } from "@/utils/tool-call-icon"; -import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path"; +import { type InlinePathTarget, parseInlinePathToken } from "@/utils/inline-path"; import { getMarkdownListMarker, getMarkdownNextSiblingType } from "@/utils/markdown-list"; import { type AssistantFileLinkSource } from "@/utils/assistant-file-link-resolver"; import { useAssistantFileLinkResolver } from "@/hooks/use-assistant-file-link-resolver"; @@ -730,21 +730,6 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({ containerCompactBottom: { paddingBottom: 0, }, - // Used in custom markdownRules for path chip styling - pathChip: { - backgroundColor: theme.colors.surface2, - borderRadius: theme.borderRadius.full, - paddingHorizontal: theme.spacing[2], - paddingVertical: 2, - marginRight: theme.spacing[1], - marginVertical: 2, - }, - pathChipText: { - color: theme.colors.foreground, - fontFamily: Fonts.mono, - fontSize: 13, - userSelect: isWeb ? "text" : "auto", - }, imageFrame: { width: "100%", minHeight: 160, @@ -1018,30 +1003,6 @@ function resolveAssistantImageErrorText(fileError: unknown, dataError: unknown): return "Unable to load image preview."; } -interface InlinePathChipProps { - content: string; - parsed: InlinePathTarget; - onPress: (target: InlinePathTarget) => void; -} - -const INLINE_PATH_CHIP_STYLE = [ - assistantMessageStylesheet.pathChip, - assistantMessageStylesheet.pathChipText, -]; - -function InlinePathChip({ content, parsed, onPress }: InlinePathChipProps) { - const handlePress = useCallback(() => onPress(parsed), [onPress, parsed]); - return ( - - {content} - - ); -} - function MarkdownLink({ source, style, @@ -1156,7 +1117,7 @@ function getMarkdownLinkSource(node: AssistantMarkdownAstNode): AssistantFileLin text: getMarkdownNodeText(node), markup: node.markup, sourceInfo: node.sourceInfo, - sourceType: node.sourceType, + sourceType: node.sourceType === "inline-code" ? "inline-code" : undefined, }; } @@ -1627,6 +1588,46 @@ function MarkdownInheritedCodeLink({ ); } +interface MarkdownInlinePathCodeLinkProps { + content: string; + inheritedStyles: TextStyle; + codeInlineStyle: TextStyle; + linkStyle: TextStyle; + onPress: (source: AssistantFileLinkSource) => void; + onPrefetch: (source: AssistantFileLinkSource) => void; +} + +function MarkdownInlinePathCodeLink({ + content, + inheritedStyles, + codeInlineStyle, + linkStyle, + onPress, + onPrefetch, +}: MarkdownInlinePathCodeLinkProps) { + const source = useMemo( + () => ({ + href: content, + text: content, + sourceType: "inline-code", + }), + [content], + ); + + return ( + + {content} + + ); +} + interface MarkdownListItemContentProps { contentStyle: ViewStyle; children: ReactNode; @@ -1811,16 +1812,19 @@ export const AssistantMessage = memo(function AssistantMessage({ ) => { const content = node.content ?? ""; const isLinkedInlineCode = nodeHasParentType(parent, "link"); - const parsed = - onInlinePathPress && !isLinkedInlineCode ? parseInlinePathToken(content) : null; + const shouldResolveInlinePath = + onInlinePathPress && !isLinkedInlineCode && parseInlinePathToken(content); - if (parsed && onInlinePathPress) { + if (shouldResolveInlinePath) { return ( - ); } diff --git a/packages/app/src/utils/assistant-file-link-resolver.test.ts b/packages/app/src/utils/assistant-file-link-resolver.test.ts index 2e279d84d..0ac22cbaf 100644 --- a/packages/app/src/utils/assistant-file-link-resolver.test.ts +++ b/packages/app/src/utils/assistant-file-link-resolver.test.ts @@ -238,6 +238,44 @@ describe("assistant file link resolver", () => { expect(result.opened).toBe(true); }); + it("resolves inline-code basename line refs through directory suggestions", async () => { + const suggestions = vi.fn(async () => + resolvedSuggestions([ + { path: "packages/server/src/server/workspace-git-service.ts", kind: "file" }, + ]), + ); + const openWorkspaceFile = vi.fn(); + const resolver = createAssistantFileLinkResolver({ + getDirectorySuggestions: suggestions, + openWorkspaceFile, + openExternalUrl: vi.fn(), + }); + + const result = await resolver.open({ + context: { ...CONTEXT, workspaceRoot: "/Users/test/project" }, + source: { + href: "workspace-git-service.ts:1553", + text: "workspace-git-service.ts:1553", + sourceType: "inline-code", + }, + }); + + expect(suggestions).toHaveBeenCalledWith({ + query: "workspace-git-service.ts", + cwd: "/Users/test/project", + includeFiles: true, + includeDirectories: false, + limit: 1, + }); + expect(openWorkspaceFile).toHaveBeenCalledWith({ + raw: "workspace-git-service.ts:1553", + path: "/Users/test/project/packages/server/src/server/workspace-git-service.ts", + lineStart: 1553, + lineEnd: undefined, + }); + expect(result.opened).toBe(true); + }); + it("keeps explicit external URLs external", async () => { const openExternalUrl = vi.fn(); const resolver = createAssistantFileLinkResolver({ @@ -391,5 +429,12 @@ describe("assistant file link resolver", () => { sourceInfo: "", }), ).toBe("http://dumm.md"); + expect( + getAssistantFileLinkToken({ + href: "workspace-git-service.ts:1553", + text: "workspace-git-service.ts:1553", + sourceType: "inline-code", + }), + ).toBe("workspace-git-service.ts:1553"); }); }); diff --git a/packages/app/src/utils/assistant-file-link-resolver.ts b/packages/app/src/utils/assistant-file-link-resolver.ts index ccde952b7..bd3dbd83b 100644 --- a/packages/app/src/utils/assistant-file-link-resolver.ts +++ b/packages/app/src/utils/assistant-file-link-resolver.ts @@ -15,7 +15,7 @@ export interface AssistantFileLinkSource { text?: string; markup?: string; sourceInfo?: string; - sourceType?: string; + sourceType?: "inline-code"; } export interface DirectorySuggestionEntry { @@ -163,7 +163,7 @@ export function createAssistantFileLinkResolver( } export function getAssistantFileLinkToken(source: AssistantFileLinkSource): string { - if (isLinkifiedSource(source)) { + if (isLinkifiedSource(source) || source.sourceType === "inline-code") { const text = source.text?.trim(); if (text && isFileLookingAssistantToken(text)) { return text;