Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ import { useEnvironmentDisconnectDelay } from "../hooks/useEnvironmentDisconnect
import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions";
import { useEnvironmentQuery } from "../state/query";
import { filesystemEnvironment } from "../state/filesystem";
import { resolveProjectFileRoots } from "../lib/projectFileRoots";
import { vcsEnvironment } from "../state/vcs";
import {
environmentServerConfigsAtom,
Expand Down Expand Up @@ -3671,11 +3673,26 @@ export default function ChatView(props: ChatViewProps) {
: null,
[multiRepoStatusRoots, gitStatusCwd],
);
// @-mention file search spans every repo root for a multi-repo workspace
// (#923); single-repo projects search the worktree-aware `gitCwd` alone.
const mentionRoots = useMemo(
() => (isMultiRepo ? (activeProject?.repoRoots ?? null) : null),
[isMultiRepo, activeProject?.repoRoots],
// The files panel and @-mention search span the folders a `.code-workspace`
// lists, while git surfaces stay on `repoRoots`. Null searches the
// worktree-aware cwd alone.
const workspaceFileQuery = useEnvironmentQuery(
projectWorkspaceFile === null
? null
: filesystemEnvironment.readWorkspaceFile({
environmentId,
input: { workspaceFilePath: projectWorkspaceFile },
}),
);
const workspaceFolders = workspaceFileQuery.data?.folders ?? null;
const projectFileRoots = useMemo(
() =>
resolveProjectFileRoots({
workspaceFolders,
repoRoots: activeProject?.repoRoots,
worktrees: threadWorktrees,
}),
[workspaceFolders, activeProject?.repoRoots, threadWorktrees],
);
// Single-repo git query scoped to the anchor root, backing the branch-sync,
// PR-checkout, and refresh flows; the multi-repo status/diff surfaces use
Expand Down Expand Up @@ -9769,7 +9786,7 @@ export default function ChatView(props: ChatViewProps) {
composerDraftTarget={composerDraftTarget}
keybindings={keybindings}
availableEditors={availableEditors}
repoRoots={isMultiRepo ? (activeProject?.repoRoots ?? undefined) : undefined}
roots={projectFileRoots ?? undefined}
relativePath={
renderedRightPanelSurface.kind === "file"
? renderedRightPanelSurface.relativePath
Expand Down Expand Up @@ -10196,7 +10213,7 @@ export default function ChatView(props: ChatViewProps) {
timelineOverflows={timelineOverflows}
onComposerOverlayHeightChange={publishComposerOverlayHeight}
onRestingChange={onComposerRestingChange}
mentionRoots={mentionRoots}
mentionRoots={projectFileRoots}
promptRef={promptRef}
composerImagesRef={composerImagesRef}
composerFilesRef={composerFilesRef}
Expand Down
18 changes: 17 additions & 1 deletion apps/web/src/components/ProjectFoldersDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,12 @@ export function ProjectFoldersDialog({
// Filesystem RPCs are driven through the environment atoms (the old imperative
// `readEnvironmentApi().filesystem.*` accessor was removed). Reads run via a
// query runner; the write goes through the command atom and re-resolves the file.
// Refresh rather than reuse the cached read: the chat view keeps it alive for
// its files panel, so a stale entry would outlive both an edit made in VS Code
// and a save from this dialog.
const readWorkspaceFile = useAtomQueryRunner(filesystemEnvironment.readWorkspaceFile, {
reportFailure: false,
refresh: true,
});
const writeWorkspaceFile = useAtomCommand(filesystemEnvironment.writeWorkspaceFile, {
reportFailure: false,
Expand Down Expand Up @@ -225,6 +229,10 @@ export function ProjectFoldersDialog({
repoRoots: resolved.repoRoots,
},
});
void readWorkspaceFile({
environmentId: target.environmentId,
input: { workspaceFilePath: workspaceFile },
});
const missing = resolved.folders.filter((folder) => !folder.exists);
if (missing.length > 0) {
toastManager.add({
Expand All @@ -247,7 +255,15 @@ export function ProjectFoldersDialog({
} finally {
setSaving(false);
}
}, [folders, onClose, target, updateProject, workspaceFile, writeWorkspaceFile]);
}, [
folders,
onClose,
readWorkspaceFile,
target,
updateProject,
workspaceFile,
writeWorkspaceFile,
]);

return (
<Dialog
Expand Down
24 changes: 19 additions & 5 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { usePrimaryEnvironmentId } from "../../state/environments";
import { runtimeModeConfig, runtimeModeOptions } from "./runtimeModeConfig";
import { useRightPanelStore } from "~/rightPanelStore";
import { AttachmentFilePreview } from "../files/AttachmentFilePreview";
import { labelForRoot } from "../files/filePath";
import { type ProjectFileRoot } from "../../lib/projectFileRoots";
import { Dialog, DialogPopup, DialogTitle } from "../ui/dialog";
import { filterComposerPullRequestMatches } from "@t3tools/shared/composerPullRequestMatches";
import { importPastedComposerText, readPastedComposerContext } from "../composerInlineTokenPaste";
Expand Down Expand Up @@ -1428,9 +1430,9 @@ export interface ChatComposerProps {
* effect, so it is current before the chat view measures the overlay.
*/
onRestingChange: (resting: boolean) => void;
// Multi-repo workspaces (#923): roots to union for @-mention file search.
// Omitted/`null` for single-repo projects, which search `gitCwd` alone.
mentionRoots: ReadonlyArray<string> | null;
// Multi-root projects (#923): roots to union for @-mention file search, with
// the label each result is described under. `null` searches `gitCwd` alone.
mentionRoots: ReadonlyArray<ProjectFileRoot> | null;

// Refs the parent needs kept in sync
promptRef: React.RefObject<string>;
Expand Down Expand Up @@ -2268,10 +2270,18 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
const settledPullRequestTextQuery =
pullRequestTextQuery === debouncedPullRequestTextQuery ? pullRequestTextQuery : null;
const isPathTrigger = composerTriggerKind === "path";
const mentionRootPaths = useMemo(
() => mentionRoots?.map(({ root }) => root) ?? null,
[mentionRoots],
);
const mentionRootLabels = useMemo(
() => new Map(mentionRoots?.map(({ root, label }) => [root, label])),
[mentionRoots],
);
const workspaceEntries = useComposerPathSearch({
environmentId,
cwd: isPathTrigger ? gitCwd : null,
roots: isPathTrigger ? mentionRoots : null,
roots: isPathTrigger ? mentionRootPaths : null,
query: isPathTrigger ? pathTriggerQuery : null,
});
const compactSlashCommandAvailable =
Expand Down Expand Up @@ -2360,7 +2370,10 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
...(entry.root ? { root: entry.root } : {}),
label: basenameOfPath(entry.path),
description: entry.root
? `${basenameOfPath(entry.root)}/${entry.parentPath ?? ""}`.replace(/\/$/, "")
? `${labelForRoot(mentionRootLabels, entry.root) ?? basenameOfPath(entry.root)}/${entry.parentPath ?? ""}`.replace(
/\/$/,
"",
)
: (entry.parentPath ?? entry.path.slice(0, Math.max(0, entry.path.lastIndexOf("/")))),
}));
}
Expand Down Expand Up @@ -2497,6 +2510,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
compactSlashCommandAvailable,
composerTrigger,
exactPullRequestLookup.data,
mentionRootLabels,
planModeUiEnabled,
pullRequestLookup.data,
pullRequestProjectId,
Expand Down
14 changes: 7 additions & 7 deletions apps/web/src/components/files/FileBreadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import {
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { useTheme } from "~/hooks/useTheme";
import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh";
import { type ProjectFileRoot } from "~/lib/projectFileRoots";
import { cn } from "~/lib/utils";
import { isAbsolutePath } from "~/terminal-links";

import {
buildRootLabels,
type FileBreadcrumb,
fileBreadcrumbChildren,
fileBreadcrumbParent,
Expand All @@ -37,8 +37,8 @@ interface FileBreadcrumbsProps {
readonly onOpenFile: (relativePath: string, root?: string) => void;
readonly projectName: string;
readonly relativePath: string;
/** Every repo root of a multi-repo project, so the repo crumb matches its file tree label. */
readonly repoRoots?: readonly string[] | undefined;
/** Every root of a multi-root project, so the root crumb matches its file tree label. */
readonly roots?: readonly ProjectFileRoot[] | undefined;
/** Repo root that `relativePath` is relative to, when it is not `cwd` (multi-repo). */
readonly root?: string | undefined;
readonly workspaceMutationId: string | null;
Expand Down Expand Up @@ -276,14 +276,14 @@ export function FileBreadcrumbs(props: FileBreadcrumbsProps) {
// A file in another repo of a multi-repo project reads "project > repo > path",
// and every crumb from the repo down browses and opens within that repo.
const repoRoot = props.root && props.root !== props.cwd ? props.root : undefined;
const repoRootsKey = props.repoRoots?.join("\0") ?? "";
const roots = props.roots;
const repoName = useMemo(() => {
if (!repoRoot) return undefined;
const label = repoRootsKey
? labelForRoot(buildRootLabels(repoRootsKey.split("\0")), repoRoot)
const label = roots
? labelForRoot(new Map(roots.map(({ root, label }) => [root, label])), repoRoot)
: undefined;
return label ?? repoRoot.split(/[\\/]/).findLast(Boolean) ?? repoRoot;
}, [repoRoot, repoRootsKey]);
}, [repoRoot, roots]);
const breadcrumbs = useMemo(
() =>
repoName === undefined
Expand Down
30 changes: 17 additions & 13 deletions apps/web/src/components/files/FileBrowserPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { readLocalApi } from "~/localApi";
import { T3_PIERRE_ICONS } from "~/pierre-icons";
import { PIERRE_TREE_UNSAFE_CSS, pierreTreeStyle } from "~/pierre-tree-theme";

import { buildRootLabels, isRootPath, labelForRoot } from "./filePath";
import { isRootPath, labelForRoot } from "./filePath";
import { type ProjectFileRoot, projectFileRootsKey } from "~/lib/projectFileRoots";
import { createFileTreeDragMentionController } from "./fileTreeDragMention";
import { areAllDirectoriesExpanded, setAllDirectoriesExpanded } from "./fileTreeExpansion";
import { buildFileTreePathUpdates } from "./fileTreePathReconciliation";
Expand All @@ -39,9 +40,10 @@ interface FileBrowserPanelProps {
selectedRoot?: string | undefined;
/** Bumped when the same path should be revealed again (e.g. re-opened from search). */
selectedPathRevealId: number;
// Multi-repo workspaces (#923): when set, list the union of these repo roots
// and group the tree by repo. Omitted/single-entry keeps single-root behavior.
repoRoots?: readonly string[] | undefined;
// Multi-root projects (#923): when set, list the union of these roots (repos,
// or the folders a `.code-workspace` lists) and group the tree by root.
// Omitted/single-entry keeps single-root behavior.
roots?: readonly ProjectFileRoot[] | undefined;
onOpenFile: (relativePath: string, root?: string) => void;
onRefreshSelectedFile?: () => void;
workspaceMutationId: string | null;
Expand Down Expand Up @@ -112,7 +114,7 @@ export default function FileBrowserPanel({
selectedPath: selectedRelativePath,
selectedRoot,
selectedPathRevealId,
repoRoots,
roots,
onOpenFile,
onRefreshSelectedFile,
workspaceMutationId,
Expand All @@ -123,18 +125,20 @@ export default function FileBrowserPanel({
// Multi-repo workspaces (#923): each repo is a top-level node named by its
// label, and every tree path below it is prefixed with that label so
// same-named files across repos don't collide.
const multiRepoRootsKey = repoRoots && repoRoots.length > 1 ? repoRoots.join("\0") : "";
const multiRootsKey = roots && roots.length > 1 ? projectFileRootsKey(roots) : "";
const { rootLabels, directoryRoots, searchRoots } = useMemo(() => {
if (!multiRepoRootsKey)
if (!multiRootsKey)
return { rootLabels: null, directoryRoots: undefined, searchRoots: undefined };
const roots = multiRepoRootsKey.split("\0");
const rootLabels = buildRootLabels(roots);
const directoryRoots = multiRootsKey.split("\0\0").map((pair) => {
const [label = "", root = ""] = pair.split("\0");
return { root, label };
});
return {
rootLabels,
directoryRoots: roots.map((root) => ({ root, label: rootLabels.get(root) ?? root })),
searchRoots: roots,
rootLabels: new Map(directoryRoots.map(({ root, label }) => [root, label])),
directoryRoots,
searchRoots: directoryRoots.map(({ root }) => root),
};
}, [multiRepoRootsKey]);
}, [multiRootsKey]);
// Tree paths sit under their repo's label, so an open from outside the tree
// (a chat link, the file picker) maps onto that key to be found and revealed.
// A repo root linked by its absolute path selects that repo's top-level node.
Expand Down
18 changes: 11 additions & 7 deletions apps/web/src/components/files/FilePreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import FileBrowserPanel from "./FileBrowserPanel";
import { FileBreadcrumbs } from "./FileBreadcrumbs";
import { FileMarkdownPreview } from "./FileMarkdownPreview";
import { isRootPath } from "./filePath";
import { type ProjectFileRoot, projectFileRootsKey } from "~/lib/projectFileRoots";
import {
type FileCommentAnnotationEntry,
type FileCommentAnnotationGroup,
Expand Down Expand Up @@ -104,8 +105,8 @@ interface FilePreviewPanelProps {
availableEditors: ReadonlyArray<EditorId>;
revealLine: number | null;
revealRequestId: number;
// Multi-repo workspaces (#923): repo roots to list/group in the file tree.
repoRoots?: readonly string[] | undefined;
// Multi-root projects (#923): roots to list/group in the file tree.
roots?: readonly ProjectFileRoot[] | undefined;
// Owning root of the currently-open file. Reads/writes resolve against this
// root (it may be a different repo than the anchor `cwd`). Null = anchor.
fileRoot?: string | null | undefined;
Expand Down Expand Up @@ -927,7 +928,7 @@ export default function FilePreviewPanel({
availableEditors,
revealLine,
revealRequestId,
repoRoots,
roots,
fileRoot,
onOpenFile,
onPendingChange,
Expand Down Expand Up @@ -971,7 +972,8 @@ export default function FilePreviewPanel({
const isDirectory =
file.isNotFile &&
(!isHostFile ||
(relativePath !== null && isRootPath([cwd, ...(repoRoots ?? [])], relativePath)));
(relativePath !== null &&
isRootPath([cwd, ...(roots?.map(({ root }) => root) ?? [])], relativePath)));
// Everything preview-related keys off previewPath; a folder has no preview.
const previewPath = isDirectory ? null : relativePath;
const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen);
Expand Down Expand Up @@ -1124,7 +1126,7 @@ export default function FilePreviewPanel({
onOpenFile={onOpenFile}
projectName={projectName}
relativePath={relativePath}
repoRoots={repoRoots}
roots={roots}
root={fileRoot ?? undefined}
workspaceMutationId={workspaceMutationId}
/>
Expand Down Expand Up @@ -1316,15 +1318,17 @@ export default function FilePreviewPanel({
: "min-w-0 flex-1",
)}
>
{/* The tree caches folders by label, so a root that changes path under
the same label must remount it rather than show the old contents. */}
<FileBrowserPanel
key={`${environmentId}:${cwd}`}
key={`${environmentId}:${cwd}:${projectFileRootsKey(roots)}`}
environmentId={environmentId}
cwd={cwd}
projectName={projectName}
selectedPath={relativePath}
selectedRoot={fileRoot ?? undefined}
selectedPathRevealId={revealRequestId}
repoRoots={repoRoots}
roots={roots}
Comment thread
LoganRupe marked this conversation as resolved.
onOpenFile={onOpenFile}
workspaceMutationId={workspaceMutationId}
{...(previewPath && !isMedia && !isPdf
Expand Down
Loading
Loading