Resolve inline code file links
This commit is contained in:
@@ -68,7 +68,7 @@ import type { AgentAttachment } from "@server/shared/messages";
|
|||||||
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
|
||||||
import { buildToolCallPresentation } from "@/tool-calls/presentation";
|
import { buildToolCallPresentation } from "@/tool-calls/presentation";
|
||||||
import { resolveToolCallIcon } from "@/utils/tool-call-icon";
|
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 { getMarkdownListMarker, getMarkdownNextSiblingType } from "@/utils/markdown-list";
|
||||||
import { type AssistantFileLinkSource } from "@/utils/assistant-file-link-resolver";
|
import { type AssistantFileLinkSource } from "@/utils/assistant-file-link-resolver";
|
||||||
import { useAssistantFileLinkResolver } from "@/hooks/use-assistant-file-link-resolver";
|
import { useAssistantFileLinkResolver } from "@/hooks/use-assistant-file-link-resolver";
|
||||||
@@ -730,21 +730,6 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
containerCompactBottom: {
|
containerCompactBottom: {
|
||||||
paddingBottom: 0,
|
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: {
|
imageFrame: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
minHeight: 160,
|
minHeight: 160,
|
||||||
@@ -1018,30 +1003,6 @@ function resolveAssistantImageErrorText(fileError: unknown, dataError: unknown):
|
|||||||
return "Unable to load image preview.";
|
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 (
|
|
||||||
<Text
|
|
||||||
onPress={handlePress}
|
|
||||||
selectable={isWeb ? undefined : false}
|
|
||||||
style={INLINE_PATH_CHIP_STYLE}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MarkdownLink({
|
function MarkdownLink({
|
||||||
source,
|
source,
|
||||||
style,
|
style,
|
||||||
@@ -1156,7 +1117,7 @@ function getMarkdownLinkSource(node: AssistantMarkdownAstNode): AssistantFileLin
|
|||||||
text: getMarkdownNodeText(node),
|
text: getMarkdownNodeText(node),
|
||||||
markup: node.markup,
|
markup: node.markup,
|
||||||
sourceInfo: node.sourceInfo,
|
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<AssistantFileLinkSource>(
|
||||||
|
() => ({
|
||||||
|
href: content,
|
||||||
|
text: content,
|
||||||
|
sourceType: "inline-code",
|
||||||
|
}),
|
||||||
|
[content],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarkdownInheritedCodeLink
|
||||||
|
source={source}
|
||||||
|
inheritedStyles={inheritedStyles}
|
||||||
|
codeInlineStyle={codeInlineStyle}
|
||||||
|
linkStyle={linkStyle}
|
||||||
|
onPress={onPress}
|
||||||
|
onPrefetch={onPrefetch}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</MarkdownInheritedCodeLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface MarkdownListItemContentProps {
|
interface MarkdownListItemContentProps {
|
||||||
contentStyle: ViewStyle;
|
contentStyle: ViewStyle;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -1811,16 +1812,19 @@ export const AssistantMessage = memo(function AssistantMessage({
|
|||||||
) => {
|
) => {
|
||||||
const content = node.content ?? "";
|
const content = node.content ?? "";
|
||||||
const isLinkedInlineCode = nodeHasParentType(parent, "link");
|
const isLinkedInlineCode = nodeHasParentType(parent, "link");
|
||||||
const parsed =
|
const shouldResolveInlinePath =
|
||||||
onInlinePathPress && !isLinkedInlineCode ? parseInlinePathToken(content) : null;
|
onInlinePathPress && !isLinkedInlineCode && parseInlinePathToken(content);
|
||||||
|
|
||||||
if (parsed && onInlinePathPress) {
|
if (shouldResolveInlinePath) {
|
||||||
return (
|
return (
|
||||||
<InlinePathChip
|
<MarkdownInlinePathCodeLink
|
||||||
key={node.key}
|
key={node.key}
|
||||||
content={content}
|
content={content}
|
||||||
parsed={parsed}
|
inheritedStyles={inheritedStyles}
|
||||||
onPress={onInlinePathPress}
|
codeInlineStyle={styles.code_inline}
|
||||||
|
linkStyle={styles.link}
|
||||||
|
onPress={handleLinkPress}
|
||||||
|
onPrefetch={handleLinkPrefetch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,44 @@ describe("assistant file link resolver", () => {
|
|||||||
expect(result.opened).toBe(true);
|
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 () => {
|
it("keeps explicit external URLs external", async () => {
|
||||||
const openExternalUrl = vi.fn();
|
const openExternalUrl = vi.fn();
|
||||||
const resolver = createAssistantFileLinkResolver({
|
const resolver = createAssistantFileLinkResolver({
|
||||||
@@ -391,5 +429,12 @@ describe("assistant file link resolver", () => {
|
|||||||
sourceInfo: "",
|
sourceInfo: "",
|
||||||
}),
|
}),
|
||||||
).toBe("http://dumm.md");
|
).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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface AssistantFileLinkSource {
|
|||||||
text?: string;
|
text?: string;
|
||||||
markup?: string;
|
markup?: string;
|
||||||
sourceInfo?: string;
|
sourceInfo?: string;
|
||||||
sourceType?: string;
|
sourceType?: "inline-code";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectorySuggestionEntry {
|
export interface DirectorySuggestionEntry {
|
||||||
@@ -163,7 +163,7 @@ export function createAssistantFileLinkResolver(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAssistantFileLinkToken(source: AssistantFileLinkSource): string {
|
export function getAssistantFileLinkToken(source: AssistantFileLinkSource): string {
|
||||||
if (isLinkifiedSource(source)) {
|
if (isLinkifiedSource(source) || source.sourceType === "inline-code") {
|
||||||
const text = source.text?.trim();
|
const text = source.text?.trim();
|
||||||
if (text && isFileLookingAssistantToken(text)) {
|
if (text && isFileLookingAssistantToken(text)) {
|
||||||
return text;
|
return text;
|
||||||
|
|||||||
Reference in New Issue
Block a user