11import { useQuery } from "@tanstack/react-query" ;
2- import { FileCodeIcon , XIcon } from "lucide-react" ;
3- import { memo , useCallback } from "react" ;
2+ import { EyeIcon , EyeOffIcon , FileCodeIcon , XIcon } from "lucide-react" ;
3+ import { memo , useCallback , useState } from "react" ;
44
55import { useCodeViewerStore , type CodeViewerTab } from "~/codeViewerStore" ;
66import { useTheme } from "~/hooks/useTheme" ;
@@ -13,6 +13,12 @@ import { MarkdownPreview } from "./MarkdownPreview";
1313import { isElectron } from "~/env" ;
1414import { Button } from "./ui/button" ;
1515
16+ /** Check if a file path is a dotenv / secrets file whose values should be masked. */
17+ function isEnvFile ( filePath : string ) : boolean {
18+ const basename = filePath . split ( "/" ) . pop ( ) ?? filePath ;
19+ return / ^ \. e n v ( \. .* ) ? $ / . test ( basename ) ;
20+ }
21+
1622export function CodeViewerTabStrip ( props : {
1723 tabs : CodeViewerTab [ ] ;
1824 activeTabPath : string | null ;
@@ -66,6 +72,9 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
6672 resolvedTheme : "light" | "dark" ;
6773 onAddContext : ( ctx : CodeContextSelection ) => void ;
6874} ) {
75+ const envFile = isEnvFile ( props . relativePath ) ;
76+ const [ envValuesRevealed , setEnvValuesRevealed ] = useState ( false ) ;
77+
6978 const query = useQuery (
7079 projectReadFileQueryOptions ( {
7180 cwd : props . cwd ,
@@ -109,22 +118,59 @@ export const CodeViewerFileContent = memo(function CodeViewerFileContent(props:
109118 }
110119
111120 return (
112- < div className = "min-h-0 flex-1 overflow-y-auto" >
121+ < div className = "relative min-h-0 flex-1 overflow-y-auto" >
113122 { query . data . truncated && (
114123 < div className = "border-b border-amber-500/30 bg-amber-500/10 px-3 py-1 text-[11px] text-amber-700 dark:text-amber-300/90" >
115124 File is larger than 1MB. Showing truncated content.
116125 </ div >
117126 ) }
118- { isMarkdownPreviewFilePath ( props . relativePath ) ? (
119- < MarkdownPreview contents = { query . data . contents } />
120- ) : (
121- < CodeMirrorViewer
122- contents = { query . data . contents }
123- filePath = { props . relativePath }
124- resolvedTheme = { props . resolvedTheme }
125- onAddContext = { props . onAddContext }
126- />
127+
128+ { /* Env file: show/hide toggle banner */ }
129+ { envFile && (
130+ < div className = "sticky top-0 z-10 flex items-center justify-between border-b border-amber-500/30 bg-amber-500/10 px-3 py-1.5" >
131+ < span className = "text-[11px] font-medium text-amber-700 dark:text-amber-300/90" >
132+ Sensitive file — values are hidden by default
133+ </ span >
134+ < Button
135+ type = "button"
136+ size = "xs"
137+ variant = "ghost"
138+ className = "gap-1.5 text-[11px] text-amber-700 hover:text-amber-900 dark:text-amber-300/90 dark:hover:text-amber-100"
139+ onClick = { ( ) => setEnvValuesRevealed ( ( prev ) => ! prev ) }
140+ >
141+ { envValuesRevealed ? (
142+ < >
143+ < EyeOffIcon className = "size-3.5" />
144+ Hide values
145+ </ >
146+ ) : (
147+ < >
148+ < EyeIcon className = "size-3.5" />
149+ Show values
150+ </ >
151+ ) }
152+ </ Button >
153+ </ div >
127154 ) }
155+
156+ < div
157+ className = { cn (
158+ envFile &&
159+ ! envValuesRevealed &&
160+ "select-none blur-[6px] transition-[filter] duration-200" ,
161+ ) }
162+ >
163+ { isMarkdownPreviewFilePath ( props . relativePath ) ? (
164+ < MarkdownPreview contents = { query . data . contents } />
165+ ) : (
166+ < CodeMirrorViewer
167+ contents = { query . data . contents }
168+ filePath = { props . relativePath }
169+ resolvedTheme = { props . resolvedTheme }
170+ onAddContext = { props . onAddContext }
171+ />
172+ ) }
173+ </ div >
128174 </ div >
129175 ) ;
130176} ) ;
0 commit comments