Refactor chat file-link opens to a single disposition seam (#1038)
* Open chat file links in a side pane on cmd-click
* Add hover tooltips to file links showing cmd-click hint
* Collapse file-link open path to a single disposition seam
Replace the parallel "open in side" callback that was threaded
alongside the normal open callback at every layer between the chat
click handler and the workspace router. One callback now carries a
`OpenFileDisposition` ("main" | "side") so the modifier-key decision
lives at the leaf and the routing decision lives at the workspace
screen, with no branching in between.
* Give assistant file links a home
This commit is contained in:
@@ -75,6 +75,7 @@ import {
|
||||
type BottomAnchorRouteRequest,
|
||||
} from "./use-bottom-anchor-controller";
|
||||
import { normalizeInlinePathTarget } from "@/utils/inline-path";
|
||||
import type { OpenFileDisposition } from "@/utils/workspace-file-open";
|
||||
import { resolveWorkspaceIdByExecutionDirectory } from "@/utils/workspace-execution";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||
@@ -242,7 +243,7 @@ export interface AgentStreamViewProps {
|
||||
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
|
||||
isAuthoritativeHistoryReady?: boolean;
|
||||
toast?: ToastApi | null;
|
||||
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
|
||||
onOpenWorkspaceFile?: (input: { filePath: string; disposition: OpenFileDisposition }) => void;
|
||||
}
|
||||
|
||||
const EMPTY_STREAM_HEAD: StreamItem[] = [];
|
||||
@@ -304,6 +305,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
});
|
||||
const openWorkspaceFile = useStableEvent(function openWorkspaceFile(input: {
|
||||
filePath: string;
|
||||
disposition: OpenFileDisposition;
|
||||
}) {
|
||||
onOpenWorkspaceFile?.(input);
|
||||
});
|
||||
@@ -323,7 +325,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
}, [agentId]);
|
||||
|
||||
const handleInlinePathPress = useCallback(
|
||||
(target: InlinePathTarget) => {
|
||||
(target: InlinePathTarget, disposition: OpenFileDisposition) => {
|
||||
if (!target.path) {
|
||||
return;
|
||||
}
|
||||
@@ -335,7 +337,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
|
||||
if (normalized.file) {
|
||||
if (onOpenWorkspaceFile) {
|
||||
openWorkspaceFile({ filePath: normalized.file });
|
||||
openWorkspaceFile({ filePath: normalized.file, disposition });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -381,7 +383,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
|
||||
|
||||
const handleToolCallOpenFile = useCallback(
|
||||
(filePath: string) => {
|
||||
handleInlinePathPress({ raw: filePath, path: filePath });
|
||||
handleInlinePathPress({ raw: filePath, path: filePath }, "main");
|
||||
},
|
||||
[handleInlinePathPress],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
type MouseEvent,
|
||||
} from "react";
|
||||
import {
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
type StyleProp,
|
||||
type TextStyle,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { isNative, isWeb } from "@/constants/platform";
|
||||
import { classifyAssistantFileLink, type InlinePathTarget } from "@/utils/inline-path";
|
||||
import type { AssistantFileLinkSource } from "@/utils/assistant-file-link-resolver";
|
||||
import type { OpenFileDisposition } from "@/utils/workspace-file-open";
|
||||
import { Shortcut } from "@/components/ui/shortcut";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
interface AssistantInlinePathLinkProps {
|
||||
content: string;
|
||||
parsed: InlinePathTarget;
|
||||
onPress: (target: InlinePathTarget, disposition: OpenFileDisposition) => void;
|
||||
workspaceRoot?: string;
|
||||
style: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export function AssistantInlinePathLink({
|
||||
content,
|
||||
parsed,
|
||||
onPress,
|
||||
workspaceRoot,
|
||||
style,
|
||||
}: AssistantInlinePathLinkProps) {
|
||||
const handlePress = useCallback(() => onPress(parsed, "main"), [onPress, parsed]);
|
||||
const handleAnchorClickCapture = useCallback(
|
||||
(event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
if (!isModifiedOpenEvent(event)) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
onPress(parsed, "side");
|
||||
},
|
||||
[onPress, parsed],
|
||||
);
|
||||
|
||||
if (!isNative) {
|
||||
return (
|
||||
<FileLinkHoverTooltip filePath={formatInlinePathTargetForTooltip(parsed, workspaceRoot)}>
|
||||
<a
|
||||
href={parsed.path}
|
||||
onClickCapture={handleAnchorClickCapture}
|
||||
onAuxClickCapture={preventAnchorNavigation}
|
||||
style={LINK_ANCHOR_STYLE}
|
||||
>
|
||||
<Text onPress={handlePress} selectable={isWeb ? undefined : false} style={style}>
|
||||
{content}
|
||||
</Text>
|
||||
</a>
|
||||
</FileLinkHoverTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text onPress={handlePress} selectable={isWeb ? undefined : false} style={style}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
interface AssistantMarkdownLinkProps {
|
||||
source: AssistantFileLinkSource;
|
||||
style: StyleProp<TextStyle>;
|
||||
onPress: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void;
|
||||
onPrefetch: (source: AssistantFileLinkSource) => void;
|
||||
workspaceRoot?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AssistantMarkdownLink({
|
||||
source,
|
||||
style,
|
||||
onPress,
|
||||
onPrefetch,
|
||||
workspaceRoot,
|
||||
children,
|
||||
}: AssistantMarkdownLinkProps) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const href = source.href;
|
||||
const handlePress = useCallback(() => onPress(source, "main"), [onPress, source]);
|
||||
const handleAnchorClickCapture = useCallback(
|
||||
(event: MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
if (!isModifiedOpenEvent(event)) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
onPress(source, "side");
|
||||
},
|
||||
[onPress, source],
|
||||
);
|
||||
const handlePrefetch = useCallback(() => onPrefetch(source), [onPrefetch, source]);
|
||||
const handleHoverIn = useCallback(() => {
|
||||
setHovered(true);
|
||||
handlePrefetch();
|
||||
}, [handlePrefetch]);
|
||||
const handleHoverOut = useCallback(() => setHovered(false), []);
|
||||
const hoveredTextStyle = useMemo<StyleProp<TextStyle>>(
|
||||
() => [style, hovered && { textDecorationLine: "underline" as const }],
|
||||
[style, hovered],
|
||||
);
|
||||
const tooltipFilePath = useMemo(
|
||||
() => getMarkdownLinkTooltipFilePath(source.href, workspaceRoot),
|
||||
[source.href, workspaceRoot],
|
||||
);
|
||||
if (isNative) {
|
||||
return (
|
||||
<Text accessibilityRole="link" onPress={handlePress} style={style}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const anchor = (
|
||||
<a
|
||||
href={href}
|
||||
onClickCapture={handleAnchorClickCapture}
|
||||
onAuxClickCapture={preventAnchorNavigation}
|
||||
style={LINK_ANCHOR_STYLE}
|
||||
>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
onPress={handlePress}
|
||||
onFocus={handlePrefetch}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
>
|
||||
<Text style={hoveredTextStyle}>{children}</Text>
|
||||
</Pressable>
|
||||
</a>
|
||||
);
|
||||
|
||||
if (tooltipFilePath) {
|
||||
return <FileLinkHoverTooltip filePath={tooltipFilePath}>{anchor}</FileLinkHoverTooltip>;
|
||||
}
|
||||
return anchor;
|
||||
}
|
||||
|
||||
interface AssistantMarkdownCodeLinkProps {
|
||||
source: AssistantFileLinkSource;
|
||||
inheritedStyles: TextStyle;
|
||||
codeInlineStyle: TextStyle;
|
||||
linkStyle: TextStyle;
|
||||
onPress: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void;
|
||||
onPrefetch: (source: AssistantFileLinkSource) => void;
|
||||
workspaceRoot?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AssistantMarkdownCodeLink({
|
||||
source,
|
||||
inheritedStyles,
|
||||
codeInlineStyle,
|
||||
linkStyle,
|
||||
onPress,
|
||||
onPrefetch,
|
||||
workspaceRoot,
|
||||
children,
|
||||
}: AssistantMarkdownCodeLinkProps) {
|
||||
const style = useMemo(
|
||||
() => [inheritedStyles, codeInlineStyle, linkStyle],
|
||||
[inheritedStyles, codeInlineStyle, linkStyle],
|
||||
);
|
||||
return (
|
||||
<AssistantMarkdownLink
|
||||
source={source}
|
||||
style={style}
|
||||
onPress={onPress}
|
||||
onPrefetch={onPrefetch}
|
||||
workspaceRoot={workspaceRoot}
|
||||
>
|
||||
{children}
|
||||
</AssistantMarkdownLink>
|
||||
);
|
||||
}
|
||||
|
||||
interface AssistantInlineCodePathLinkProps {
|
||||
content: string;
|
||||
inheritedStyles: TextStyle;
|
||||
codeInlineStyle: TextStyle;
|
||||
linkStyle: TextStyle;
|
||||
onPress: (source: AssistantFileLinkSource, disposition: OpenFileDisposition) => void;
|
||||
onPrefetch: (source: AssistantFileLinkSource) => void;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export function AssistantInlineCodePathLink({
|
||||
content,
|
||||
inheritedStyles,
|
||||
codeInlineStyle,
|
||||
linkStyle,
|
||||
onPress,
|
||||
onPrefetch,
|
||||
workspaceRoot,
|
||||
}: AssistantInlineCodePathLinkProps) {
|
||||
const source = useMemo<AssistantFileLinkSource>(
|
||||
() => ({
|
||||
href: content,
|
||||
text: content,
|
||||
sourceType: "inline-code",
|
||||
}),
|
||||
[content],
|
||||
);
|
||||
|
||||
return (
|
||||
<AssistantMarkdownCodeLink
|
||||
source={source}
|
||||
inheritedStyles={inheritedStyles}
|
||||
codeInlineStyle={codeInlineStyle}
|
||||
linkStyle={linkStyle}
|
||||
onPress={onPress}
|
||||
onPrefetch={onPrefetch}
|
||||
workspaceRoot={workspaceRoot}
|
||||
>
|
||||
{content}
|
||||
</AssistantMarkdownCodeLink>
|
||||
);
|
||||
}
|
||||
|
||||
function getMarkdownLinkTooltipFilePath(
|
||||
href: string,
|
||||
workspaceRoot: string | undefined,
|
||||
): string | null {
|
||||
const classification = classifyAssistantFileLink(href, { workspaceRoot });
|
||||
if (classification?.kind !== "directFile") {
|
||||
return null;
|
||||
}
|
||||
return formatInlinePathTargetForTooltip(classification.target, workspaceRoot);
|
||||
}
|
||||
|
||||
function formatInlinePathTargetForTooltip(
|
||||
target: InlinePathTarget,
|
||||
workspaceRoot: string | undefined,
|
||||
): string {
|
||||
let result = relativizePathToWorkspace(target.path, workspaceRoot);
|
||||
if (target.lineStart) {
|
||||
result += `:${target.lineStart}`;
|
||||
if (target.lineEnd && target.lineEnd !== target.lineStart) {
|
||||
result += `-${target.lineEnd}`;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function relativizePathToWorkspace(filePath: string, workspaceRoot: string | undefined): string {
|
||||
if (!workspaceRoot) {
|
||||
return filePath;
|
||||
}
|
||||
const root = workspaceRoot.replace(/\/+$/, "");
|
||||
if (!root) {
|
||||
return filePath;
|
||||
}
|
||||
if (filePath === root) {
|
||||
return ".";
|
||||
}
|
||||
const prefix = `${root}/`;
|
||||
if (filePath.startsWith(prefix)) {
|
||||
return filePath.slice(prefix.length);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
const FILE_LINK_TOOLTIP_TRIGGER_STYLE: ViewStyle = {
|
||||
// RN doesn't type "inline-flex" but RN-web honors it at runtime, which keeps
|
||||
// the tooltip wrapper from breaking inline link flow.
|
||||
display: "inline-flex" as ViewStyle["display"],
|
||||
};
|
||||
|
||||
const FILE_LINK_TOOLTIP_MOD_KEYS = ["mod"];
|
||||
|
||||
function FileLinkHoverTooltip({ filePath, children }: { filePath: string; children: ReactNode }) {
|
||||
if (!isWeb) {
|
||||
return children;
|
||||
}
|
||||
return (
|
||||
<Tooltip delayDuration={400}>
|
||||
<TooltipTrigger asChild>
|
||||
<View style={FILE_LINK_TOOLTIP_TRIGGER_STYLE}>{children}</View>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start" maxWidth={520}>
|
||||
<View style={styles.tooltipBody}>
|
||||
<Text selectable={false} style={styles.tooltipPath}>
|
||||
{filePath}
|
||||
</Text>
|
||||
<View style={styles.tooltipHintRow}>
|
||||
<Shortcut keys={FILE_LINK_TOOLTIP_MOD_KEYS} />
|
||||
<Text selectable={false} style={styles.tooltipHintText}>
|
||||
click for side pane
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const LINK_ANCHOR_STYLE: CSSProperties = {
|
||||
display: "contents",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
};
|
||||
|
||||
function preventAnchorNavigation(event: MouseEvent<HTMLAnchorElement>): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function isModifiedOpenEvent(event: MouseEvent<HTMLElement>): boolean {
|
||||
return event.metaKey || event.ctrlKey;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
tooltipBody: {
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
tooltipPath: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
tooltipHintRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
tooltipHintText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
}));
|
||||
@@ -68,7 +68,8 @@ 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 { type InlinePathTarget, parseInlinePathToken } from "@/utils/inline-path";
|
||||
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
||||
import type { OpenFileDisposition } from "@/utils/workspace-file-open";
|
||||
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";
|
||||
@@ -91,6 +92,11 @@ import {
|
||||
import { PlanCard } from "./plan-card";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||
import {
|
||||
AssistantInlineCodePathLink,
|
||||
AssistantMarkdownCodeLink,
|
||||
AssistantMarkdownLink,
|
||||
} from "./assistant-file-link";
|
||||
import { getCompactionMarkerLabel } from "./message-compaction-label";
|
||||
import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url";
|
||||
import { persistAttachmentFromBytes, persistAttachmentFromDataUrl } from "@/attachments/service";
|
||||
@@ -709,7 +715,7 @@ export const LiveElapsed = memo(function LiveElapsed({
|
||||
interface AssistantMessageProps {
|
||||
message: string;
|
||||
timestamp: number;
|
||||
onInlinePathPress?: (target: InlinePathTarget) => void;
|
||||
onInlinePathPress?: (target: InlinePathTarget, disposition: OpenFileDisposition) => void;
|
||||
workspaceRoot?: string;
|
||||
serverId?: string;
|
||||
client?: DaemonClient | null;
|
||||
@@ -1001,70 +1007,6 @@ function resolveAssistantImageErrorText(fileError: unknown, dataError: unknown):
|
||||
return "Unable to load image preview.";
|
||||
}
|
||||
|
||||
function MarkdownLink({
|
||||
source,
|
||||
style,
|
||||
onPress,
|
||||
onPrefetch,
|
||||
children,
|
||||
}: {
|
||||
source: AssistantFileLinkSource;
|
||||
style: StyleProp<TextStyle>;
|
||||
onPress: (source: AssistantFileLinkSource) => void;
|
||||
onPrefetch: (source: AssistantFileLinkSource) => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const href = source.href;
|
||||
const handlePress = useCallback(() => onPress(source), [onPress, source]);
|
||||
const handlePrefetch = useCallback(() => onPrefetch(source), [onPrefetch, source]);
|
||||
const handleHoverIn = useCallback(() => {
|
||||
setHovered(true);
|
||||
handlePrefetch();
|
||||
}, [handlePrefetch]);
|
||||
const handleHoverOut = useCallback(() => setHovered(false), []);
|
||||
const hoveredTextStyle = useMemo<StyleProp<TextStyle>>(
|
||||
() => [style, hovered && { textDecorationLine: "underline" as const }],
|
||||
[style, hovered],
|
||||
);
|
||||
if (isNative) {
|
||||
return (
|
||||
<Text accessibilityRole="link" onPress={handlePress} style={style}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClickCapture={preventAnchorNavigation}
|
||||
onAuxClickCapture={preventAnchorNavigation}
|
||||
style={MARKDOWN_LINK_ANCHOR_STYLE}
|
||||
>
|
||||
<Pressable
|
||||
accessibilityRole="link"
|
||||
onPress={handlePress}
|
||||
onFocus={handlePrefetch}
|
||||
onHoverIn={handleHoverIn}
|
||||
onHoverOut={handleHoverOut}
|
||||
>
|
||||
<Text style={hoveredTextStyle}>{children}</Text>
|
||||
</Pressable>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const MARKDOWN_LINK_ANCHOR_STYLE: React.CSSProperties = {
|
||||
display: "contents",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
};
|
||||
|
||||
function preventAnchorNavigation(event: React.MouseEvent<HTMLAnchorElement>): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function getInlineCodeAutoLinkUrl(
|
||||
markdownParser: ReturnType<typeof MarkdownIt>,
|
||||
content: string,
|
||||
@@ -1556,76 +1498,6 @@ function MarkdownInheritedText({
|
||||
return <Text style={style}>{children}</Text>;
|
||||
}
|
||||
|
||||
interface MarkdownInheritedCodeLinkProps {
|
||||
source: AssistantFileLinkSource;
|
||||
inheritedStyles: TextStyle;
|
||||
codeInlineStyle: TextStyle;
|
||||
linkStyle: TextStyle;
|
||||
onPress: (source: AssistantFileLinkSource) => void;
|
||||
onPrefetch: (source: AssistantFileLinkSource) => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function MarkdownInheritedCodeLink({
|
||||
source,
|
||||
inheritedStyles,
|
||||
codeInlineStyle,
|
||||
linkStyle,
|
||||
onPress,
|
||||
onPrefetch,
|
||||
children,
|
||||
}: MarkdownInheritedCodeLinkProps) {
|
||||
const style = useMemo(
|
||||
() => [inheritedStyles, codeInlineStyle, linkStyle],
|
||||
[inheritedStyles, codeInlineStyle, linkStyle],
|
||||
);
|
||||
return (
|
||||
<MarkdownLink source={source} style={style} onPress={onPress} onPrefetch={onPrefetch}>
|
||||
{children}
|
||||
</MarkdownLink>
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
contentStyle: ViewStyle;
|
||||
children: ReactNode;
|
||||
@@ -1718,8 +1590,8 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
});
|
||||
|
||||
const handleLinkPress = useCallback(
|
||||
(source: AssistantFileLinkSource) => {
|
||||
fileLinkResolver.open({ source });
|
||||
(source: AssistantFileLinkSource, disposition: OpenFileDisposition) => {
|
||||
fileLinkResolver.open({ source, disposition });
|
||||
},
|
||||
[fileLinkResolver],
|
||||
);
|
||||
@@ -1731,7 +1603,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
);
|
||||
const handleMarkdownLinkPress = useCallback(
|
||||
(url: string) => {
|
||||
fileLinkResolver.open({ source: { href: url } });
|
||||
fileLinkResolver.open({ source: { href: url }, disposition: "main" });
|
||||
// react-native-markdown-display opens the link itself when this returns true.
|
||||
// We already handled it above, so return false to avoid duplicate opens.
|
||||
return false;
|
||||
@@ -1815,7 +1687,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
|
||||
if (shouldResolveInlinePath) {
|
||||
return (
|
||||
<MarkdownInlinePathCodeLink
|
||||
<AssistantInlineCodePathLink
|
||||
key={node.key}
|
||||
content={content}
|
||||
inheritedStyles={inheritedStyles}
|
||||
@@ -1823,6 +1695,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
linkStyle={styles.link}
|
||||
onPress={handleLinkPress}
|
||||
onPrefetch={handleLinkPrefetch}
|
||||
workspaceRoot={workspaceRoot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1834,7 +1707,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
content,
|
||||
});
|
||||
return (
|
||||
<MarkdownInheritedCodeLink
|
||||
<AssistantMarkdownCodeLink
|
||||
key={node.key}
|
||||
source={source}
|
||||
inheritedStyles={inheritedStyles}
|
||||
@@ -1842,9 +1715,10 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
linkStyle={styles.link}
|
||||
onPress={handleLinkPress}
|
||||
onPrefetch={handleLinkPrefetch}
|
||||
workspaceRoot={workspaceRoot}
|
||||
>
|
||||
{content}
|
||||
</MarkdownInheritedCodeLink>
|
||||
</AssistantMarkdownCodeLink>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1916,12 +1790,13 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
</MarkdownParagraphView>
|
||||
),
|
||||
link: (node: ASTNode, children: ReactNode[], _parent: ASTNode[], styles: MarkdownStyles) => (
|
||||
<MarkdownLink
|
||||
<AssistantMarkdownLink
|
||||
key={node.key}
|
||||
source={getMarkdownLinkSource(node)}
|
||||
style={styles.link}
|
||||
onPress={handleLinkPress}
|
||||
onPrefetch={handleLinkPrefetch}
|
||||
workspaceRoot={workspaceRoot}
|
||||
>
|
||||
{Children.map(children, (child) => {
|
||||
if (!isValidElement(child)) return child;
|
||||
@@ -1930,7 +1805,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
style: [childProps.style, { color: styles.link.color }],
|
||||
} as Partial<{ style: StyleProp<TextStyle> }>);
|
||||
})}
|
||||
</MarkdownLink>
|
||||
</AssistantMarkdownLink>
|
||||
),
|
||||
image: (
|
||||
node: ASTNode,
|
||||
|
||||
Reference in New Issue
Block a user