Skip to content
Closed
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
70 changes: 19 additions & 51 deletions apps/web/src/components/ChatMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,6 @@ import { MediaActions, type MediaActionSource } from "./media/MediaActions";
import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent";
import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";
import { PierreEntryIcon } from "./chat/PierreEntryIcon";
import {
revealInFileExplorerLabelForKind,
revealInFileExplorerLabelForOs,
} from "./preview/fileExplorerLabel";
import {
resolveExternalWebLinkHost,
showExternalLinkContextMenu,
Expand Down Expand Up @@ -139,6 +135,7 @@ import { HighlightedCodeLines } from "./chat/HighlightedCodeLines";
import { RenderErrorBoundary } from "./RenderErrorBoundary";
import { useTheme } from "../hooks/useTheme";
import { getClientSettings, useClientSettings } from "../hooks/useSettings";
import { runFileManagerPath, useFileManagerActionForEnvironment } from "../fileManagerReveal";
import {
chatMarkdownClipboardPayload,
serializeTableElementToCsv,
Expand All @@ -163,7 +160,6 @@ import { useRemoteOpenResolution, type RemoteOpenMode } from "../remoteOpen";
import { useRightPanelStore } from "../rightPanelStore";
import { readThreadShell, useProjects } from "../state/entities";
import { serverEnvironment } from "../state/server";
import { shellEnvironment } from "../state/shell";
import { assetEnvironment } from "../state/assets";
import { usePreparedConnection } from "../state/session";
import { previewEnvironment } from "../state/preview";
Expand Down Expand Up @@ -1988,38 +1984,16 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
if (!onReveal) {
return;
}
void (async () => {
try {
const result = await onReveal();
if (result._tag === "Success" || isAtomCommandInterrupted(result)) {
return;
}
reportMarkdownActionFailure(
{ operation: "reveal-file-in-file-manager", target: targetPath },
result.cause,
);
const error = squashAtomCommandFailure(result);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Unable to reveal file",
description: error instanceof Error ? error.message : "An error occurred.",
}),
);
} catch (cause) {
void runFileManagerPath(
() => onReveal(),
targetPath,
"Unable to reveal file",
(cause) =>
reportMarkdownActionFailure(
{ operation: "reveal-file-in-file-manager", target: targetPath },
cause,
);
toastManager.add(
stackedThreadToast({
type: "error",
title: "Unable to reveal file",
description: cause instanceof Error ? cause.message : "An error occurred.",
}),
);
}
})();
),
);
}, [onReveal, targetPath]);

const handleCopy = useCallback(
Expand Down Expand Up @@ -2343,17 +2317,10 @@ function useChatMarkdownState({
const [preferredEditor] = usePreferredEditor(availableEditors);
const preferredEditorMenuLabel = openInEditorMenuLabel(preferredEditor);
const openInPreferredEditor = useOpenInPreferredEditor(environmentId, availableEditors);
const openInEditor = useAtomCommand(shellEnvironment.openInEditor, {
reportFailure: false,
});
const revealInFileManagerLabel =
environmentId !== null &&
serverConfig?.shellRevealInFileManager === true &&
serverConfig.availableEditors.includes("file-manager")
? serverConfig.shellRevealInFileManagerKind === undefined
? revealInFileExplorerLabelForOs(serverConfig.environment.platform.os)
: revealInFileExplorerLabelForKind(serverConfig.shellRevealInFileManagerKind)
: undefined;
const environmentFileManagerAction = useFileManagerActionForEnvironment(environmentId);
const fileManagerAction = canUseShellActions ? environmentFileManagerAction : null;
const fileManagerReveal = fileManagerAction?.reveal ?? null;
const revealInFileManagerLabel = fileManagerReveal?.label;
const revealFileInFileManager = useCallback(
(filePath: string) => {
if (environmentId === null) {
Expand All @@ -2363,12 +2330,12 @@ function useChatMarkdownState({
),
);
}
return openInEditor({
environmentId,
input: { cwd: filePath, editor: "file-manager", reveal: true },
});
if (fileManagerReveal === null) {
return Promise.reject(new Error("File-manager reveal is unavailable."));
}
return fileManagerReveal.run(filePath);
},
[environmentId, openInEditor],
[environmentId, fileManagerReveal],
);
const diffThemeName = resolveDiffThemeName(resolvedTheme);
const markdownFileLinkMetaByHref = useMemo(() => {
Expand Down Expand Up @@ -2623,7 +2590,7 @@ function useChatMarkdownState({
}
openInEditorMenuLabel={preferredEditorMenuLabel}
onReveal={
canUseShellActions && revealInFileManagerLabel !== undefined
canUseShellActions && fileManagerReveal !== null
? () => revealMarkdownFileInFileManager(fileLinkMeta)
: undefined
}
Expand All @@ -2641,6 +2608,7 @@ function useChatMarkdownState({
},
[
canUseShellActions,
fileManagerReveal,
fileLinkParentSuffixByPath,
openFileInPanel,
openInPreferredEditor,
Expand Down
41 changes: 41 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ interface PersistentThreadTerminalDrawerProps {
closeShortcutLabel: string | undefined;
keybindings: ResolvedKeybindingsConfig;
onAddTerminalContext: (selection: TerminalContextSelection) => void;
onAddTerminalLink?: (text: string) => void;
}

const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDrawer({
Expand All @@ -911,6 +912,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
closeShortcutLabel,
keybindings,
onAddTerminalContext,
onAddTerminalLink,
}: PersistentThreadTerminalDrawerProps) {
const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");
const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write");
Expand Down Expand Up @@ -1211,6 +1213,16 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
[onAddTerminalContext, visible],
);

const handleAddTerminalLink = useCallback(
(text: string) => {
if (!visible || !onAddTerminalLink) {
return;
}
onAddTerminalLink(text);
},
[onAddTerminalLink, visible],
);

if (!project || (!terminalUiState.terminalOpen && !active) || !cwd) {
return null;
}
Expand Down Expand Up @@ -1252,6 +1264,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
onCloseTerminal={closeTerminal}
onHeightChange={setTerminalHeight}
onAddTerminalContext={handleAddTerminalContext}
onAddTerminalLink={onAddTerminalLink ? handleAddTerminalLink : undefined}
terminalLabelsById={terminalLabelsById}
terminalLaunchLocationsById={terminalLaunchLocationsById}
/>
Expand All @@ -1268,6 +1281,7 @@ interface PersistentThreadTerminalPanelProps {
focusRequestId: number;
keybindings: ResolvedKeybindingsConfig;
onAddTerminalContext: (selection: TerminalContextSelection) => void;
onAddTerminalLink?: (text: string) => void;
onSplitTerminal: () => void;
onSplitTerminalVertical: () => void;
onNewTerminal: () => void;
Expand All @@ -1287,6 +1301,7 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane
focusRequestId,
keybindings,
onAddTerminalContext,
onAddTerminalLink,
onSplitTerminal,
onSplitTerminalVertical,
onNewTerminal,
Expand Down Expand Up @@ -1337,6 +1352,15 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane
: {},
[project, worktreePath],
);
const handleAddTerminalLink = useCallback(
(text: string) => {
if (!visible || !onAddTerminalLink) {
return;
}
onAddTerminalLink(text);
},
[onAddTerminalLink, visible],
);
const terminalLabelsById = useMemo(() => {
const labels = new Map<string, string>();
for (const terminalId of surface.terminalIds) {
Expand Down Expand Up @@ -1425,6 +1449,7 @@ const PersistentThreadTerminalPanel = memo(function PersistentThreadTerminalPane
onCloseTerminal={onCloseTerminal}
onHeightChange={() => undefined}
onAddTerminalContext={onAddTerminalContext}
onAddTerminalLink={onAddTerminalLink ? handleAddTerminalLink : undefined}
terminalLabelsById={terminalLabelsById}
terminalLaunchLocationsById={terminalLaunchLocationsById}
keybindings={keybindings}
Expand Down Expand Up @@ -4028,6 +4053,18 @@ export default function ChatView(props: ChatViewProps) {
},
[composerRef],
);
const addTerminalLinkToDraft = useCallback(
(text: string) => {
if (!composerRef.current?.insertTextAtEnd(`${text} `, { ensureLeadingBoundary: true })) {
toastManager.add({
type: "error",
title: "Unable to add to chat",
description: "The composer is busy; try again once it is ready.",
});
}
},
[composerRef],
);
const setTerminalOpen = useCallback(
(open: boolean) => {
if (!activeThreadRef) return;
Expand Down Expand Up @@ -9621,6 +9658,7 @@ export default function ChatView(props: ChatViewProps) {
focusRequestId={terminalFocusRequestId}
keybindings={keybindings}
onAddTerminalContext={addTerminalContextToDraft}
onAddTerminalLink={addTerminalLinkToDraft}
onSplitTerminal={splitPanelTerminal}
onSplitTerminalVertical={splitPanelTerminalVertical}
onNewTerminal={addTerminalSurface}
Expand Down Expand Up @@ -10320,6 +10358,7 @@ export default function ChatView(props: ChatViewProps) {
closeShortcutLabel={closeTerminalShortcutLabel ?? undefined}
keybindings={keybindings}
onAddTerminalContext={addTerminalContextToDraft}
onAddTerminalLink={addTerminalLinkToDraft}
/>
))}
</div>
Expand All @@ -10332,6 +10371,7 @@ export default function ChatView(props: ChatViewProps) {
maximized={rightPanelMaximized}
surfaces={renderedRightPanelSurfaces}
environmentId={activeThreadRef.environmentId}
workspaceRoot={activeWorkspaceRoot ?? null}
activeSurfaceId={renderedRightPanelSurface?.id ?? null}
pendingSurfaceIds={pendingFileSurfaceIds}
previewSessions={activePreviewState.sessions}
Expand Down Expand Up @@ -10390,6 +10430,7 @@ export default function ChatView(props: ChatViewProps) {
}
surfaces={renderedRightPanelSurfaces}
environmentId={activeThreadRef.environmentId}
workspaceRoot={activeWorkspaceRoot ?? null}
activeSurfaceId={renderedRightPanelSurface?.id ?? null}
pendingSurfaceIds={pendingFileSurfaceIds}
previewSessions={activePreviewState.sessions}
Expand Down
35 changes: 35 additions & 0 deletions apps/web/src/components/LegacySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import { ensureLocalApi, readLocalApi } from "../localApi";
import { useComposerDraftStore } from "../composerDraftStore";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { useDesktopUpdateState } from "../state/desktopUpdate";
import { openFileManagerPath, useFileManagerAction } from "../fileManagerReveal";

import { useThreadActions } from "../hooks/useThreadActions";
import { projectEnvironment } from "../state/projects";
Expand Down Expand Up @@ -214,6 +215,7 @@ import {
type SidebarProjectSnapshot,
} from "../sidebarProjectGrouping";
import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons";
import { openWorkspaceMenuLabel } from "./threadActionMenu.logic";
const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = {
updated_at: "Last user message",
created_at: "Created at",
Expand Down Expand Up @@ -1133,6 +1135,7 @@ interface SidebarProjectItemProps {
handleNewThread: ReturnType<typeof useNewThreadHandler>;
archiveThread: ReturnType<typeof useThreadActions>["archiveThread"];
deleteThread: ReturnType<typeof useThreadActions>["deleteThread"];
resolveFileManagerAction: ReturnType<typeof useFileManagerAction>;
threadJumpLabelByKey: ReadonlyMap<string, string>;
attachThreadListAutoAnimateRef: (node: HTMLElement | null) => void;
expandThreadListForProject: (projectKey: string) => void;
Expand All @@ -1154,6 +1157,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
handleNewThread,
archiveThread,
deleteThread,
resolveFileManagerAction,
threadJumpLabelByKey,
attachThreadListAutoAnimateRef,
expandThreadListForProject,
Expand Down Expand Up @@ -2239,6 +2243,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
);
const threadWorkspacePath =
thread.worktreePath ?? threadProject?.workspaceRoot ?? project.workspaceRoot ?? null;
const fileManagerAction = threadWorkspacePath
? resolveFileManagerAction(thread.environmentId)
: null;
const clicked = await api.contextMenu.show(
[
...(thread.branch
Expand All @@ -2248,6 +2255,18 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
{ id: "mark-unread", label: "Mark unread" },
{ id: "copy-path", label: "Copy Path" },
{ id: "copy-thread-id", label: "Copy Thread ID" },
...(fileManagerAction
? [
{
id: "open-in-file-manager",
label: openWorkspaceMenuLabel(
fileManagerAction.open.managerName,
thread.worktreePath !== null,
),
icon: "folder" as const,
},
]
: []),
{ id: "project-settings", label: "Project settings" },
{ id: "delete", label: "Delete", destructive: true, icon: "trash" },
],
Expand Down Expand Up @@ -2314,6 +2333,15 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
copyThreadIdToClipboard(thread.id, { threadId: thread.id });
return;
}
if (clicked === "open-in-file-manager") {
if (!threadWorkspacePath || !fileManagerAction) return;
await openFileManagerPath(
fileManagerAction,
threadWorkspacePath,
`Failed to open ${thread.worktreePath ? "worktree" : "project"}`,
);
return;
}
if (clicked !== "delete") return;
if (appSettingsConfirmThreadDelete) {
const confirmed = await api.dialogs.confirm(
Expand Down Expand Up @@ -2350,6 +2378,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
memberProjectByScopedKey,
project.projectKey,
project.workspaceRoot,
resolveFileManagerAction,
router,
setOpenMobile,
startThreadRename,
Expand Down Expand Up @@ -2887,6 +2916,7 @@ interface SidebarProjectsContentProps {
handleNewThread: ReturnType<typeof useNewThreadHandler>;
archiveThread: ReturnType<typeof useThreadActions>["archiveThread"];
deleteThread: ReturnType<typeof useThreadActions>["deleteThread"];
resolveFileManagerAction: ReturnType<typeof useFileManagerAction>;
sortedProjects: readonly SidebarProjectSnapshot[];
expandedThreadListsByProject: ReadonlySet<string>;
activeRouteProjectKey: string | null;
Expand Down Expand Up @@ -2929,6 +2959,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
handleNewThread,
archiveThread,
deleteThread,
resolveFileManagerAction,
sortedProjects,
expandedThreadListsByProject,
activeRouteProjectKey,
Expand Down Expand Up @@ -3080,6 +3111,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
handleNewThread={handleNewThread}
archiveThread={archiveThread}
deleteThread={deleteThread}
resolveFileManagerAction={resolveFileManagerAction}
threadJumpLabelByKey={threadJumpLabelByKey}
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
expandThreadListForProject={expandThreadListForProject}
Expand Down Expand Up @@ -3113,6 +3145,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
handleNewThread={handleNewThread}
archiveThread={archiveThread}
deleteThread={deleteThread}
resolveFileManagerAction={resolveFileManagerAction}
threadJumpLabelByKey={threadJumpLabelByKey}
attachThreadListAutoAnimateRef={attachThreadListAutoAnimateRef}
expandThreadListForProject={expandThreadListForProject}
Expand Down Expand Up @@ -3149,6 +3182,7 @@ export default function LegacySidebar() {
const updateSettings = useUpdateClientSettings();
const handleNewThread = useNewThreadHandler();
const { archiveThread, deleteThread } = useThreadActions();
const resolveFileManagerAction = useFileManagerAction();
const { isMobile, setOpenMobile } = useSidebar();
const routeTarget = useParams({
strict: false,
Expand Down Expand Up @@ -3800,6 +3834,7 @@ export default function LegacySidebar() {
handleNewThread={handleNewThread}
archiveThread={archiveThread}
deleteThread={deleteThread}
resolveFileManagerAction={resolveFileManagerAction}
sortedProjects={sortedProjects}
expandedThreadListsByProject={expandedThreadListsByProject}
activeRouteProjectKey={activeRouteProjectKey}
Expand Down
Loading
Loading