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
80 changes: 76 additions & 4 deletions crates/agent-gateway/web/src/app/GatewayAppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { WorkspaceCloneModal } from "@liveagent/ui/components/chat/WorkspaceClon
import { WorkspaceCloneTaskOverlay } from "@liveagent/ui/components/chat/WorkspaceCloneTaskOverlay";
import { WorkspaceProjectSettingsModal } from "@liveagent/ui/components/chat/WorkspaceProjectSettingsModal";
import { ChevronDown } from "@liveagent/ui/components/IconSet";
import { FileTreePaneSurface } from "@liveagent/ui/components/project-tools/file-tree/index";
import { ProjectToolsPanelToggle } from "@liveagent/ui/components/project-tools/ProjectToolsPanelToggle";
import { RightDockPanel } from "@liveagent/ui/components/project-tools/RightDockPanel";
import { TrajectoryView } from "@liveagent/ui/components/trajectory/TrajectoryView";
Expand All @@ -21,6 +22,7 @@ import { UnsupportedPaneSurface } from "@liveagent/ui/components/workbench/surfa
import { WorkbenchCanvas } from "@liveagent/ui/components/workbench/WorkbenchCanvas";
import { WorkbenchEmptyState } from "@liveagent/ui/components/workbench/WorkbenchEmptyState";
import { WorkspaceOverlayHost } from "@liveagent/ui/components/workspace-editor/WorkspaceOverlayHost";
import { isWorkspacePreviewPath } from "@liveagent/ui/components/workspace-editor/workspaceImagePreview";
import { LocaleContext, t as translate } from "@liveagent/ui/i18n/index";
import {
type CheckpointRewindClient,
Expand All @@ -34,7 +36,7 @@ import { cn } from "@liveagent/ui/lib/shared/utils";
import { useSidebarSelector } from "@liveagent/ui/lib/sidebar/useSidebarSelector";
import { toTrajectoryMessages } from "@liveagent/ui/lib/trajectory/transcriptMessages";
import { useConversationViewState } from "@liveagent/ui/lib/trajectory/useConversationViewState";
import { hitTestWorkbenchDrop } from "@liveagent/ui/lib/workbench/index";
import { findPaneIdBySurfaceKey, hitTestWorkbenchDrop } from "@liveagent/ui/lib/workbench/index";
import type { PaneRecord } from "@liveagent/ui/lib/workbench/types";
import { ChatComposerBar } from "@liveagent/ui/pages/chat/ChatComposerBar";
import { FloorNavRail } from "@liveagent/ui/pages/chat/transcript/FloorNavRail";
Expand All @@ -59,7 +61,9 @@ import { GatewayTranscript } from "@/components/GatewayTranscript";
import type { SttProviderId } from "@/lib/settings";
import {
getNextTheme,
getRightDockFileTreeState,
updateExecutionModeFromChatSelection,
updateRightDockFileTreeState,
updateSystem,
updateWorkspaceResourceSettings,
workspaceProjectPathKey,
Expand Down Expand Up @@ -464,6 +468,8 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
switch (surface.kind) {
case "conversation":
return sidebarConversationsById.get(surface.conversationId)?.title?.trim() || "";
case "fileTree":
return translate("projectTools.fileTreeTitle", settings.locale);
case "localTerminal":
return surface.launchSpec.title?.trim() || surface.launchSpec.shell?.trim() || "Terminal";
case "sshTerminal":
Expand All @@ -472,7 +478,7 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
return surface.originalKind;
}
},
[sidebarConversationsById],
[settings.locale, sidebarConversationsById],
);

const workbenchHasMultiplePanes =
Expand Down Expand Up @@ -897,6 +903,54 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
/>
);
}
if (surface.kind === "fileTree") {
const project = workspaceProjects.find(
(entry) => workspaceProjectPathKey(entry.path) === surface.project.projectPathKey,
);
if (!project) {
return (
<UnsupportedPaneSurface paneId={pane.paneId} originalKind="fileTree:missing" />
);
}
return (
<FileTreePaneSurface
active
projectPathKey={surface.project.projectPathKey}
cwd={project.path}
state={getRightDockFileTreeState(
settings.customSettings,
surface.project.projectPathKey,
)}
workspaceProject={project}
workspaceProjectRootClient={workspaceProjectRootClient}
workspaceRootRevision={workspaceRootRevision}
workspaceActivityClient={workspaceActivityClient}
onStateChange={(patch) =>
setSettings((current) =>
updateRightDockFileTreeState(current, surface.project.projectPathKey, patch),
)
}
onInsertFileMention={
surface.project.projectPathKey === terminalProjectPathKey
? handleRightDockInsertFileMention
: undefined
}
onOpenFile={(path, imagePaths) => {
const request = {
projectPathKey: surface.project.projectPathKey,
workdir: project.path,
path,
imagePaths,
};
if (isWorkspacePreviewPath(path)) {
openWorkspaceFilePreview(request);
} else {
openWorkspaceEditorFile(request);
}
}}
/>
);
}
if (surface.kind === "unsupported") {
return (
<UnsupportedPaneSurface paneId={pane.paneId} originalKind={surface.originalKind} />
Expand Down Expand Up @@ -1003,11 +1057,15 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
const workbenchDragGhost =
sessionWorkbench.enabled && workbenchController.dragState ? (
<div
ref={workbenchController.dragGhostRef}
data-workbench-drag-ghost=""
className="layer-popover pointer-events-none fixed max-w-[220px] truncate rounded-md border border-border bg-background/95 px-2.5 py-1 text-xs text-foreground shadow-md"
style={{
left: workbenchController.dragState.pointer.x + 14,
top: workbenchController.dragState.pointer.y + 10,
left: 0,
top: 0,
transform:
"translate3d(var(--workbench-drag-ghost-x, -9999px), var(--workbench-drag-ghost-y, -9999px), 0)",
willChange: "transform",
}}
>
{workbenchController.dragState.payload.title ||
Expand Down Expand Up @@ -1677,6 +1735,12 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
sessions={terminalSessions}
sessionsLoaded={terminalSessionsLoaded}
leasedSessionIds={workbenchLeasedDockSessionIds}
fileTreeLeased={Boolean(
findPaneIdBySurfaceKey(
workbenchController.workbench.layout,
`fileTree:${terminalProjectPathKey}`,
),
)}
width={settings.customSettings.rightDock.width}
theme={effectiveTheme}
disabledMessage={projectToolsDisabledMessage}
Expand Down Expand Up @@ -1713,6 +1777,14 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
onOpenTerminalInWorkbench={
sessionWorkbench.enabled ? workbenchController.handleOpenTerminalInSplit : undefined
}
onFileTreeTabDragStart={
sessionWorkbench.enabled
? workbenchController.handleFileTreeTabDragIntent
: undefined
}
onOpenFileTreeInWorkbench={
sessionWorkbench.enabled ? workbenchController.handleOpenFileTreeInSplit : undefined
}
onOpenNewTerminalInWorkbench={
sessionWorkbench.enabled
? workbenchController.handleOpenNewTerminalInSplit
Expand Down
84 changes: 81 additions & 3 deletions crates/agent-gateway/web/src/app/workbench/useGatewayWorkbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { TerminalClient, TerminalSession } from "@liveagent/ui/lib/terminal
import {
commitWorkspaceDropConversation,
findAdjacentPaneId,
findPaneIdBySurfaceKey,
findParentSplitId,
MIN_CONVERSATION_PANE_HEIGHT,
MIN_CONVERSATION_PANE_WIDTH,
Expand Down Expand Up @@ -39,7 +40,7 @@ import {
} from "@liveagent/ui/lib/workbench/useWindowWorkbench";
import {
useWorkbenchDragSession,
type WorkbenchDragState,
type WorkbenchDragRenderState,
type WorkbenchDragUnavailableReason,
type WorkbenchDropCommit,
} from "@liveagent/ui/lib/workbench/useWorkbenchDragSession";
Expand Down Expand Up @@ -125,6 +126,7 @@ export type GatewayWorkbenchController = {
handleOpenConversationInSplit: (item: SidebarConversation) => boolean;
/** Right Dock 菜单「在分屏中打开」:同一 drop 事务的无拖拽入口。 */
handleOpenTerminalInSplit: (session: TerminalSession) => void;
handleOpenFileTreeInSplit: () => void;
/** Right Dock 新建菜单「在分屏中新建终端」。 */
handleOpenNewTerminalInSplit: () => void;
/** SSH overlay「已在画板中打开」占位的「前往 Pane」聚焦通路。 */
Expand All @@ -135,7 +137,9 @@ export type GatewayWorkbenchController = {
clearWorkbench: () => void;
projectRefForConversation: (item: { id: string; cwd?: string | null }) => ProjectRef;
/** 拖拽 overlay 模型(幽灵 + 落点预览);idle 时为 null。 */
dragState: WorkbenchDragState | null;
dragState: WorkbenchDragRenderState | null;
/** Imperative compositor-only pointer tracking for the drag ghost. */
dragGhostRef: (element: HTMLDivElement | null) => void;
/** Pane 头部拖动把手(pointer-down 发起)。 */
beginPaneDrag: (
pane: PaneRecord,
Expand Down Expand Up @@ -184,6 +188,12 @@ export type GatewayWorkbenchController = {
clientY: number;
currentTarget?: EventTarget | null;
}) => void;
handleFileTreeTabDragIntent: (event: {
pointerId: number;
clientX: number;
clientY: number;
currentTarget?: EventTarget | null;
}) => void;
/** 被画布 Pane 租用的会话(Right Dock 终端 tab 互斥隐藏用)。 */
leasedDockSessionIds: readonly string[];
};
Expand Down Expand Up @@ -620,6 +630,22 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
commitTerminalDrop(payload, target, terminalDropDeps());
return;
}
if (payload.kind === "fileTree") {
const existingPaneId = findPaneIdBySurfaceKey(
workbench.layoutRef.current,
`fileTree:${payload.project.projectPathKey}`,
);
if (target.kind === "pane-center") {
if (existingPaneId && target.paneId === existingPaneId) handleFocusPane(existingPaneId);
return;
}
if (existingPaneId) {
if (target.kind !== "canvas-empty") workbench.movePane(existingPaneId, target);
return;
}
workbench.openFileTreeSurface({ kind: "fileTree", project: payload.project }, target);
return;
}
// Pane 头部拖动重排。
if (target.kind === "canvas-empty") return;
if (target.kind === "pane-center" && target.paneId === payload.paneId) return;
Expand Down Expand Up @@ -648,7 +674,7 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
],
);

const { dragState, beginDrag } = useWorkbenchDragSession({
const { dragState, beginDrag, dragGhostRef } = useWorkbenchDragSession({
enabled,
layoutRef: workbench.layoutRef,
geometryRef,
Expand Down Expand Up @@ -791,6 +817,33 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
[beginDrag, newTerminalTitle, terminalProjectPath],
);

const fileTreeProjectRef = useCallback((): ProjectRef | null => {
const path = terminalProjectPath.trim();
if (!path) return null;
const projectPathKey = workspaceProjectPathKey(path);
const project = workspaceProjectsRef.current.find(
(entry) => workspaceProjectPathKey(entry.path) === projectPathKey,
);
return {
projectId: project?.id ?? `project:${projectPathKey}`,
projectPathKey,
};
}, [terminalProjectPath]);

const handleFileTreeTabDragIntent = useCallback(
(event: {
pointerId: number;
clientX: number;
clientY: number;
currentTarget?: EventTarget | null;
}) => {
const project = fileTreeProjectRef();
if (!project) return;
beginDrag({ kind: "fileTree", project, title: "File Tree" }, event);
},
[beginDrag, fileTreeProjectRef],
);

// 同一提交通路的菜单入口:终端 tab 无需拖拽也能进工作台。已租用的会话
// 由 commitTerminalDrop 自己走「移动既有 Pane」,不会二次开 Pane。
const handleOpenTerminalInSplit = useCallback(
Expand Down Expand Up @@ -821,6 +874,25 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
[onNoSpaceForSplit, resolveAutoDockTarget, terminalDropDeps],
);

const handleOpenFileTreeInSplit = useCallback(() => {
const project = fileTreeProjectRef();
if (!project) return;
const existingPaneId = findPaneIdBySurfaceKey(
workbench.layoutRef.current,
`fileTree:${project.projectPathKey}`,
);
if (existingPaneId) {
handleFocusPane(existingPaneId);
return;
}
const target = resolveAutoDockTarget();
if (!target) {
onNoSpaceForSplit();
return;
}
workbench.openFileTreeSurface({ kind: "fileTree", project }, target);
}, [fileTreeProjectRef, handleFocusPane, onNoSpaceForSplit, resolveAutoDockTarget, workbench]);

const handleOpenNewTerminalInSplit = useCallback(() => {
const target = resolveAutoDockTarget();
if (!target) {
Expand Down Expand Up @@ -908,17 +980,20 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
handleClosePane,
handleOpenConversationInSplit,
handleOpenTerminalInSplit,
handleOpenFileTreeInSplit,
handleOpenNewTerminalInSplit,
focusTerminalPaneForSession,
closePanesForRemovedConversations,
clearWorkbench,
projectRefForConversation,
dragState,
dragGhostRef,
beginPaneDrag,
handleConversationDragIntent,
handleProjectDragIntent,
handleTerminalTabDragIntent,
handleNewTerminalDragIntent,
handleFileTreeTabDragIntent,
leasedDockSessionIds,
}),
[
Expand All @@ -928,17 +1003,20 @@ export function useGatewayWorkbench(params: UseGatewayWorkbenchParams): GatewayW
handleClosePane,
handleOpenConversationInSplit,
handleOpenTerminalInSplit,
handleOpenFileTreeInSplit,
handleOpenNewTerminalInSplit,
focusTerminalPaneForSession,
closePanesForRemovedConversations,
clearWorkbench,
projectRefForConversation,
dragState,
dragGhostRef,
beginPaneDrag,
handleConversationDragIntent,
handleProjectDragIntent,
handleTerminalTabDragIntent,
handleNewTerminalDragIntent,
handleFileTreeTabDragIntent,
leasedDockSessionIds,
],
);
Expand Down
Loading
Loading