File size: 3,742 Bytes
9b906ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import { createContext, ReactNode, useContext } from "react";
import { useOptionalConversationId } from "#/hooks/use-conversation-id";
import { openWorkspaceFile } from "#/services/canvas-ui";
import EventLogger from "#/utils/event-logger";

/**
 * When false, PathComponent renders a non-interactive span. EventGroup's
 * header wraps titles in a toggle <button>, so nested path buttons would be
 * invalid markup — that context sets this to false.
 */
export const PathInteractiveContext = createContext(true);

/**
 * Decodes HTML entities in a string
 * @param text The text to decode
 * @returns The decoded text
 */
const decodeHtmlEntities = (text: string): string => {
  const textarea = document.createElement("textarea");
  textarea.innerHTML = text;
  return textarea.value;
};

/**
 * Checks if a path is likely a directory
 * @param path The full path
 * @returns True if the path is likely a directory
 */
const isLikelyDirectory = (path: string): boolean => {
  if (!path) return false;
  // Check if path already ends with a slash
  if (path.endsWith("/") || path.endsWith("\\")) return true;
  // Check if path has no extension (simple heuristic)
  const lastPart = path.split(/[/\\]/).pop() || "";
  // If the last part has no dots, it's likely a directory
  return !lastPart.includes(".");
};

/**
 * Extracts the filename from a path
 * @param path The full path
 * @returns The filename (last part of the path)
 */
const extractFilename = (path: string): string => {
  if (!path) return "";
  // Handle both Unix and Windows paths
  const parts = path.split(/[/\\]/);
  const filename = parts[parts.length - 1];

  // Add trailing slash for directories
  if (isLikelyDirectory(path) && !filename.endsWith("/")) {
    return `${filename}/`;
  }

  return filename;
};

/**
 * Displays only the filename, with the full path on hover.
 * Click opens the Files drawer on that path (when interactive).
 */
function PathComponent(props: { children?: ReactNode }) {
  const { children } = props;
  const { conversationId } = useOptionalConversationId();
  const interactive = useContext(PathInteractiveContext);

  const processPath = (path: string) => {
    try {
      const decodedPath = decodeHtmlEntities(path);
      const filename = extractFilename(decodedPath);
      if (!interactive) {
        return (
          <span
            className="font-mono font-normal tracking-tight"
            title={decodedPath}
          >
            {filename}
          </span>
        );
      }
      return (
        <button
          type="button"
          data-testid="path-component-link"
          className="cursor-pointer font-mono font-normal tracking-tight hover:underline"
          title={decodedPath}
          onClick={(event) => {
            event.stopPropagation();
            openWorkspaceFile(decodedPath, conversationId);
          }}
        >
          {filename}
        </button>
      );
    } catch (e) {
      EventLogger.error(String(e));
      return (
        <span className="font-mono font-normal tracking-tight">{path}</span>
      );
    }
  };

  if (Array.isArray(children)) {
    const processedChildren = children.map((child, index) =>
      typeof child === "string" ? (
        <span key={`${child}-${index}`}>{processPath(child)}</span>
      ) : (
        child
      ),
    );

    return (
      <span className="font-normal tracking-tight">{processedChildren}</span>
    );
  }

  if (typeof children === "string") {
    return (
      <span className="font-normal tracking-tight">
        {processPath(children)}
      </span>
    );
  }

  return (
    <span className="font-mono font-normal tracking-tight">{children}</span>
  );
}

export { PathComponent, isLikelyDirectory };