From 08d16912e43667f757d68eff81acdc6a0d87df78 Mon Sep 17 00:00:00 2001 From: marviny <369795172@qq.com> Date: Wed, 10 Jun 2026 20:51:33 +0800 Subject: [PATCH] feat(files): long-press to copy relative file path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File browser rows now support long-press → Copy path, copying FileNode.path (relative path) to the clipboard so users can paste it into chat to @-reference files without manual typing. Co-authored-by: Cursor --- .../ui/files/FileBrowserPane.kt | 95 ++++++++++++++----- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/yage/opencode_client/ui/files/FileBrowserPane.kt b/app/src/main/java/com/yage/opencode_client/ui/files/FileBrowserPane.kt index 80c12a1..95c347c 100644 --- a/app/src/main/java/com/yage/opencode_client/ui/files/FileBrowserPane.kt +++ b/app/src/main/java/com/yage/opencode_client/ui/files/FileBrowserPane.kt @@ -1,6 +1,12 @@ package com.yage.opencode_client.ui.files -import androidx.compose.foundation.clickable +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -9,14 +15,22 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.automirrored.filled.InsertDriveFile +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Folder +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.yage.opencode_client.data.model.FileNode import com.yage.opencode_client.ui.theme.AddedFile @@ -41,12 +55,16 @@ internal fun FileBrowserPane( } } +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun FileRow( file: FileNode, status: String?, onClick: () -> Unit ) { + val context = LocalContext.current + var showMenu by remember { mutableStateOf(false) } + val statusColor = when (status) { "added" -> AddedFile "modified" -> ModifiedFile @@ -54,30 +72,57 @@ internal fun FileRow( else -> if (status == "untracked") UntrackedFile else null } - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = if (file.isDirectory) Icons.Default.Folder else Icons.AutoMirrored.Filled.InsertDriveFile, - contentDescription = null, - tint = statusColor ?: MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = file.name, - style = MaterialTheme.typography.bodyLarge, - color = statusColor ?: MaterialTheme.colorScheme.onSurface - ) - if (file.ignored == true) { - Spacer(modifier = Modifier.width(8.dp)) + Box { + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = { showMenu = true } + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = if (file.isDirectory) Icons.Default.Folder else Icons.AutoMirrored.Filled.InsertDriveFile, + contentDescription = null, + tint = statusColor ?: MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.width(12.dp)) Text( - text = "ignored", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline + text = file.name, + style = MaterialTheme.typography.bodyLarge, + color = statusColor ?: MaterialTheme.colorScheme.onSurface + ) + if (file.ignored == true) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "ignored", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline + ) + } + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + DropdownMenuItem( + text = { Text("Copy path") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = null + ) + }, + onClick = { + showMenu = false + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("file path", file.path) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, "Path copied", Toast.LENGTH_SHORT).show() + } ) } }