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