workspace file search (#2002)
This commit is contained in:
committed by
GitHub
parent
82a4e0fccf
commit
7bc8ece068
@@ -26,6 +26,7 @@ use db::models::{
|
||||
coding_agent_turn::CodingAgentTurn,
|
||||
execution_process::{ExecutionProcess, ExecutionProcessRunReason, ExecutionProcessStatus},
|
||||
merge::{Merge, MergeStatus, PrMerge, PullRequestInfo},
|
||||
project::SearchResult,
|
||||
repo::{Repo, RepoError},
|
||||
session::{CreateSession, Session},
|
||||
task::{Task, TaskRelationships, TaskStatus},
|
||||
@@ -45,6 +46,7 @@ use git2::BranchType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use services::services::{
|
||||
container::ContainerService,
|
||||
file_search_cache::SearchQuery,
|
||||
git::{ConflictOp, GitCliError, GitServiceError},
|
||||
workspace_manager::WorkspaceManager,
|
||||
};
|
||||
@@ -1568,6 +1570,43 @@ pub async fn get_task_attempt_repos(
|
||||
Ok(ResponseJson(ApiResponse::success(repos)))
|
||||
}
|
||||
|
||||
pub async fn search_workspace_files(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
Query(search_query): Query<SearchQuery>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<SearchResult>>>, StatusCode> {
|
||||
if search_query.q.trim().is_empty() {
|
||||
return Ok(ResponseJson(ApiResponse::error(
|
||||
"Query parameter 'q' is required and cannot be empty",
|
||||
)));
|
||||
}
|
||||
|
||||
let repos =
|
||||
match WorkspaceRepo::find_repos_for_workspace(&deployment.db().pool, workspace.id).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to get workspace repos: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
};
|
||||
|
||||
match deployment
|
||||
.project()
|
||||
.search_files(
|
||||
deployment.file_search_cache().as_ref(),
|
||||
&repos,
|
||||
&search_query,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(results) => Ok(ResponseJson(ApiResponse::success(results))),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to search files: {}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_first_user_message(
|
||||
Extension(workspace): Extension<Workspace>,
|
||||
State(deployment): State<DeploymentImpl>,
|
||||
@@ -1724,6 +1763,7 @@ pub fn router(deployment: &DeploymentImpl) -> Router<DeploymentImpl> {
|
||||
.route("/change-target-branch", post(change_target_branch))
|
||||
.route("/rename-branch", post(rename_branch))
|
||||
.route("/repos", get(get_task_attempt_repos))
|
||||
.route("/search", get(search_workspace_files))
|
||||
.route("/first-message", get(get_first_user_message))
|
||||
.route("/mark-seen", put(mark_seen))
|
||||
.layer(from_fn_with_state(
|
||||
|
||||
@@ -553,6 +553,7 @@ export function SessionChatBoxContainer({
|
||||
return (
|
||||
<SessionChatBox
|
||||
status={status}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
editor={{
|
||||
value: editorValue,
|
||||
|
||||
@@ -32,6 +32,7 @@ interface ChatBoxBaseProps {
|
||||
placeholder: string;
|
||||
onCmdEnter: () => void;
|
||||
disabled?: boolean;
|
||||
workspaceId?: string;
|
||||
projectId?: string;
|
||||
autoFocus?: boolean;
|
||||
|
||||
@@ -81,6 +82,7 @@ export function ChatBoxBase({
|
||||
placeholder,
|
||||
onCmdEnter,
|
||||
disabled,
|
||||
workspaceId,
|
||||
projectId,
|
||||
autoFocus,
|
||||
variant,
|
||||
@@ -142,6 +144,7 @@ export function ChatBoxBase({
|
||||
onCmdEnter={onCmdEnter}
|
||||
disabled={disabled}
|
||||
className="min-h-0 max-h-[50vh] overflow-y-auto"
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
autoFocus={autoFocus}
|
||||
onPasteFiles={onPasteFiles}
|
||||
|
||||
@@ -129,6 +129,7 @@ interface SessionChatBoxProps {
|
||||
reviewComments?: ReviewCommentsProps;
|
||||
toolbarActions?: ToolbarActionsProps;
|
||||
error?: string | null;
|
||||
workspaceId?: string;
|
||||
projectId?: string;
|
||||
agent?: BaseCodingAgent | null;
|
||||
executor?: ExecutorProps;
|
||||
@@ -153,6 +154,7 @@ export function SessionChatBox({
|
||||
reviewComments,
|
||||
toolbarActions,
|
||||
error,
|
||||
workspaceId,
|
||||
projectId,
|
||||
agent,
|
||||
executor,
|
||||
@@ -488,6 +490,7 @@ export function SessionChatBox({
|
||||
placeholder={placeholder}
|
||||
onCmdEnter={handleCmdEnter}
|
||||
disabled={isDisabled}
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
autoFocus={true}
|
||||
focusKey={focusKey}
|
||||
|
||||
@@ -56,7 +56,10 @@ type WysiwygProps = {
|
||||
disabled?: boolean;
|
||||
onPasteFiles?: (files: File[]) => void;
|
||||
className?: string;
|
||||
projectId?: string; // for file search in typeahead
|
||||
/** Workspace ID for workspace-scoped file search (preferred over projectId) */
|
||||
workspaceId?: string;
|
||||
/** Project ID for file search in typeahead (fallback if workspaceId not provided) */
|
||||
projectId?: string;
|
||||
onCmdEnter?: () => void;
|
||||
onShiftCmdEnter?: () => void;
|
||||
/** Task attempt ID for resolving .vibe-images paths (preferred over taskId) */
|
||||
@@ -85,6 +88,7 @@ function WYSIWYGEditor({
|
||||
disabled = false,
|
||||
onPasteFiles,
|
||||
className,
|
||||
workspaceId,
|
||||
projectId,
|
||||
onCmdEnter,
|
||||
onShiftCmdEnter,
|
||||
@@ -251,7 +255,10 @@ function WYSIWYGEditor({
|
||||
{autoFocus && <AutoFocusPlugin />}
|
||||
<HistoryPlugin />
|
||||
<MarkdownShortcutPlugin transformers={extendedTransformers} />
|
||||
<FileTagTypeaheadPlugin projectId={projectId} />
|
||||
<FileTagTypeaheadPlugin
|
||||
workspaceId={workspaceId}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<KeyboardCommandsPlugin
|
||||
onCmdEnter={onCmdEnter}
|
||||
onShiftCmdEnter={onShiftCmdEnter}
|
||||
|
||||
@@ -62,7 +62,13 @@ function getMenuPosition(anchorEl: HTMLElement) {
|
||||
return { top, bottom, left };
|
||||
}
|
||||
|
||||
export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
|
||||
export function FileTagTypeaheadPlugin({
|
||||
workspaceId,
|
||||
projectId,
|
||||
}: {
|
||||
workspaceId?: string;
|
||||
projectId?: string;
|
||||
}) {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [options, setOptions] = useState<FileTagOption[]>([]);
|
||||
const lastMousePositionRef = useRef<{ x: number; y: number } | null>(null);
|
||||
@@ -77,7 +83,7 @@ export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
|
||||
}
|
||||
|
||||
// Here query is a string, including possible empty string ''
|
||||
searchTagsAndFiles(query, projectId)
|
||||
searchTagsAndFiles(query, { workspaceId, projectId })
|
||||
.then((results) => {
|
||||
setOptions(results.map((r) => new FileTagOption(r)));
|
||||
})
|
||||
@@ -85,7 +91,7 @@ export function FileTagTypeaheadPlugin({ projectId }: { projectId?: string }) {
|
||||
console.error('Failed to search tags/files', err);
|
||||
});
|
||||
},
|
||||
[projectId]
|
||||
[workspaceId, projectId]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -565,6 +565,18 @@ export const attemptsApi = {
|
||||
return handleApiResponse<void>(response);
|
||||
},
|
||||
|
||||
searchFiles: async (
|
||||
workspaceId: string,
|
||||
query: string,
|
||||
mode?: string
|
||||
): Promise<SearchResult[]> => {
|
||||
const modeParam = mode ? `&mode=${encodeURIComponent(mode)}` : '';
|
||||
const response = await makeRequest(
|
||||
`/api/task-attempts/${workspaceId}/search?q=${encodeURIComponent(query)}${modeParam}`
|
||||
);
|
||||
return handleApiResponse<SearchResult[]>(response);
|
||||
},
|
||||
|
||||
runAgentSetup: async (
|
||||
attemptId: string,
|
||||
data: RunAgentSetupRequest
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { projectsApi, tagsApi } from '@/lib/api';
|
||||
import { attemptsApi, projectsApi, tagsApi } from '@/lib/api';
|
||||
import type { SearchResult, Tag } from 'shared/types';
|
||||
|
||||
interface FileSearchResult extends SearchResult {
|
||||
@@ -11,9 +11,14 @@ export interface SearchResultItem {
|
||||
file?: FileSearchResult;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
workspaceId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export async function searchTagsAndFiles(
|
||||
query: string,
|
||||
projectId?: string
|
||||
options?: SearchOptions
|
||||
): Promise<SearchResultItem[]> {
|
||||
const results: SearchResultItem[] = [];
|
||||
|
||||
@@ -24,9 +29,16 @@ export async function searchTagsAndFiles(
|
||||
);
|
||||
results.push(...filteredTags.map((tag) => ({ type: 'tag' as const, tag })));
|
||||
|
||||
// Fetch files (if projectId is available and query has content)
|
||||
if (projectId && query.length > 0) {
|
||||
const fileResults = await projectsApi.searchFiles(projectId, query);
|
||||
// Fetch files - prefer workspace-scoped if available
|
||||
if (query.length > 0) {
|
||||
let fileResults: SearchResult[] = [];
|
||||
if (options?.workspaceId) {
|
||||
fileResults = await attemptsApi.searchFiles(options.workspaceId, query);
|
||||
} else if (options?.projectId) {
|
||||
fileResults = await projectsApi.searchFiles(options.projectId, query);
|
||||
}
|
||||
|
||||
if (fileResults.length > 0) {
|
||||
const fileSearchResults: FileSearchResult[] = fileResults.map((item) => ({
|
||||
...item,
|
||||
name: item.path.split('/').pop() || item.path,
|
||||
@@ -35,6 +47,7 @@ export async function searchTagsAndFiles(
|
||||
...fileSearchResults.map((file) => ({ type: 'file' as const, file }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user