Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useLocale } from "../../i18n";
import { cn } from "../../lib/shared/utils";
import type { TerminalShellOption } from "../../lib/terminal/types";
import { ChevronRight, Cpu, Plus, Terminal } from "../icons";
import { ChevronDown, ChevronRight, Cpu, Plus, Terminal } from "../icons";
import { buttonVariants } from "../ui/button";
import {
DropdownMenu,
Expand Down Expand Up @@ -34,6 +34,7 @@ type RightDockCreateMenuProps = RightDockLauncherActions & {
};

type RightDockChooserProps = RightDockLauncherActions & {
shellOptions: TerminalShellOption[];
terminalReady: boolean;
terminalDisabledMessage?: string;
disabledMessage?: string;
Expand Down Expand Up @@ -105,7 +106,7 @@ export function RightDockCreateMenu(props: RightDockCreateMenuProps) {
title={t("projectTools.newProjectTool")}
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground",
"h-7 w-7 rounded-md text-muted-foreground/80 hover:text-foreground",
)}
>
<Plus className="h-4 w-4" />
Expand Down Expand Up @@ -134,6 +135,7 @@ export function RightDockCreateMenu(props: RightDockCreateMenuProps) {

export function RightDockChooser(props: RightDockChooserProps) {
const {
shellOptions,
terminalReady,
terminalDisabledMessage,
disabledMessage,
Expand Down Expand Up @@ -184,26 +186,67 @@ export function RightDockChooser(props: RightDockChooserProps) {
<p className="text-xs text-muted-foreground">{t("projectTools.getStartedHint")}</p>
</div>
<div className="flex w-full max-w-xs flex-col gap-2">
{tools.map((tool) => (
<button
key={tool.key}
type="button"
onClick={tool.onClick}
disabled={tool.disabled}
title={tool.titleAttr}
className="group flex items-center gap-3 rounded-lg border border-border/60 bg-background px-3.5 py-3 text-left text-sm text-foreground transition-all hover:border-border hover:bg-muted/60 hover:shadow-sm disabled:pointer-events-none disabled:opacity-50"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors group-hover:bg-muted group-hover:text-foreground">
{tool.icon}
</div>
<div className="min-w-0 flex-1">
<div className="font-medium leading-tight">{tool.title}</div>
<div className="mt-0.5 text-xs leading-tight text-muted-foreground">
{tool.description}
{tools.map((tool) => {
// 终端卡片在有多个 shell 时是分裂按钮:主区开默认 shell,右缘
// 箭头展开 shell 选择菜单(能力与原「+」菜单的子菜单一致)。
const splitShellMenu = tool.key === "terminal" && shellOptions.length > 1;
const card = (
<button
type="button"
onClick={tool.onClick}
disabled={tool.disabled}
title={tool.titleAttr}
className={cn(
"group flex w-full items-center gap-3 rounded-lg border border-border/60 bg-background px-3.5 py-3 text-left text-sm text-foreground transition-all hover:border-border hover:bg-muted/60 hover:shadow-sm disabled:pointer-events-none disabled:opacity-50",
splitShellMenu && "pr-12",
)}
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted/80 text-muted-foreground transition-colors group-hover:bg-muted group-hover:text-foreground">
{tool.icon}
</div>
<div className="min-w-0 flex-1">
<div className="font-medium leading-tight">{tool.title}</div>
<div className="mt-0.5 text-xs leading-tight text-muted-foreground">
{tool.description}
</div>
</div>
</button>
);
if (!splitShellMenu) {
return <div key={tool.key}>{card}</div>;
}
return (
<div key={tool.key} className="relative">
{card}
<DropdownMenu>
<DropdownMenuTrigger
disabled={tool.disabled}
title={t("projectTools.newTerminal")}
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"absolute right-2 top-1/2 h-8 w-8 -translate-y-1/2 rounded-md text-muted-foreground hover:text-foreground",
)}
>
<ChevronDown className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={6} className="min-w-36">
{shellOptions.map((option) => (
<DropdownMenuItem
key={option.id}
onSelect={() => onCreateTerminal(option.id)}
disabled={tool.disabled}
className="gap-2 text-xs"
title={option.command || option.label}
>
<Terminal className="h-3.5 w-3.5" />
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</button>
))}
);
})}
</div>
{loading ? (
<div className="text-center text-xs text-muted-foreground">{t("projectTools.loading")}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
className={cn(
"project-tools-panel zone-font-scale fixed inset-x-0 bottom-0 z-40 flex h-[min(72vh,34rem)] min-h-0 w-full shrink-0 flex-col overflow-hidden bg-background shadow-2xl transition-[opacity,transform] duration-200 ease-out motion-reduce:transition-none md:relative md:inset-auto md:z-10 md:h-full md:overflow-visible md:shadow-none",
isOpen
? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-l md:border-t-0"
? "pointer-events-auto translate-y-0 border-t border-border opacity-100 md:w-[var(--project-tools-panel-width)] md:translate-x-0 md:border-t-0"
: "pointer-events-none translate-y-full border-t border-transparent opacity-0 md:translate-x-3 md:translate-y-0 md:border-l-0 md:border-t-0",
effectiveWidthCollapsed ? "md:w-0" : "md:w-[var(--project-tools-panel-width)]",
)}
Expand All @@ -749,6 +749,15 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
{effectiveShouldRenderContent ? (
<>
<div className="project-tools-panel-handle" aria-hidden="true" />
{/* 分栏竖线:贯穿到顶,但顶端由透明渐入实色(~3rem 处),顶部
条带视觉上保持连续;有头部时实色段恰好从横线处开始衔接。 */}
<div
aria-hidden="true"
className="absolute inset-y-0 left-0 hidden w-px md:block"
style={{
background: "linear-gradient(to bottom, transparent, hsl(var(--border)) 3rem)",
}}
/>
<button
type="button"
aria-label={t("projectTools.resizePanel")}
Expand All @@ -768,61 +777,70 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
)}
/>
</button>
<div className="project-tools-panel-header flex h-11 shrink-0 items-center gap-2 border-b border-border px-3">
{/* 空态(启动页)不渲染头部行:「新建」按钮浮动在启动页右上角。 */}
{!showRightDockChooser ? (
<div
className="project-tools-panel-tabs-shell flex min-w-0 flex-1 flex-col justify-center gap-1"
onWheel={handleTabsWheel}
className="project-tools-panel-header flex h-11 shrink-0 items-center gap-2 border-b border-border px-3"
style={{ paddingRight: "calc(0.75rem + var(--win-chrome-reserve, 0px))" }}
>
<div
ref={tabsScrollRef}
className="project-tools-panel-tabs flex h-8 min-w-0 items-center gap-1 overflow-x-auto overflow-y-hidden"
className="project-tools-panel-tabs-shell flex min-w-0 flex-1 flex-col justify-center gap-1"
onWheel={handleTabsWheel}
>
<RightDockTabStrip
tabs={orderedProjectTabs}
currentActiveTab={currentActiveTab}
backgroundTasksRunning={backgroundTasksRunning}
onCloseBackgroundTasks={closeBackgroundTasks}
activeSession={activeSession}
pendingCloseSessionId={pendingCloseSessionId}
closingSessionIds={closingSessionIds}
draggingTabId={draggingTabId}
renderTabDragHandle={renderTabDragHandle}
getTabDragProps={getTabDragProps}
getTabDragStyle={getTabDragStyle}
consumeSuppressedTabClick={consumeSuppressedTabClick}
onActivateTab={activateTab}
onActivateTerminalSession={activateTerminalSession}
onCloseToolTab={closeToolTab}
onCloseTerminalRequest={handleCloseRequest}
/>
<div
ref={tabsScrollRef}
className="project-tools-panel-tabs flex h-8 min-w-0 items-center gap-1 overflow-x-auto overflow-y-hidden"
>
<RightDockTabStrip
tabs={orderedProjectTabs}
currentActiveTab={currentActiveTab}
backgroundTasksRunning={backgroundTasksRunning}
onCloseBackgroundTasks={closeBackgroundTasks}
activeSession={activeSession}
pendingCloseSessionId={pendingCloseSessionId}
closingSessionIds={closingSessionIds}
draggingTabId={draggingTabId}
renderTabDragHandle={renderTabDragHandle}
getTabDragProps={getTabDragProps}
getTabDragStyle={getTabDragStyle}
consumeSuppressedTabClick={consumeSuppressedTabClick}
onActivateTab={activateTab}
onActivateTerminalSession={activateTerminalSession}
onCloseToolTab={closeToolTab}
onCloseTerminalRequest={handleCloseRequest}
/>
{/* 新建按钮紧跟最后一个标签(浏览器标签栏式) */}
<div className="shrink-0">
<RightDockCreateMenu
open={createMenuOpen}
onOpenChange={setCreateMenuOpen}
shellOptions={shellOptions}
terminalReady={terminalReady}
terminalDisabledMessage={terminalDisabledMessage}
projectReady={projectReady}
tunnelAvailable={tunnelAvailable}
creating={creating}
onCreateTerminal={createTerminal}
onStartTool={startToolTab}
onOpenBackgroundTasks={openBackgroundTasks}
/>
</div>
</div>
<RightDockTabsScrollbar scrollRef={tabsScrollRef} />
</div>
<RightDockTabsScrollbar scrollRef={tabsScrollRef} />
{onClose ? (
<Button
variant="ghost"
size="icon"
onClick={onClose}
title={t("projectTools.closePanel")}
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground md:hidden"
>
<X className="h-4 w-4" />
</Button>
) : null}
</div>
<RightDockCreateMenu
open={createMenuOpen}
onOpenChange={setCreateMenuOpen}
shellOptions={shellOptions}
terminalReady={terminalReady}
terminalDisabledMessage={terminalDisabledMessage}
projectReady={projectReady}
tunnelAvailable={tunnelAvailable}
creating={creating}
onCreateTerminal={createTerminal}
onStartTool={startToolTab}
onOpenBackgroundTasks={openBackgroundTasks}
/>
{onClose ? (
<Button
variant="ghost"
size="icon"
onClick={onClose}
title={t("projectTools.closePanel")}
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground md:hidden"
>
<X className="h-4 w-4" />
</Button>
) : null}
</div>
) : null}

{pendingCloseSession ? (
<div className="flex shrink-0 items-center gap-2 border-b border-destructive/20 bg-destructive/5 px-3 py-2 text-xs text-destructive">
Expand Down Expand Up @@ -862,19 +880,35 @@ export const RightDockPanel = memo(function RightDockPanel(props: RightDockPanel
{disabledMessage}
</div>
) : showRightDockChooser ? (
<RightDockChooser
terminalReady={terminalReady}
terminalDisabledMessage={terminalDisabledMessage}
disabledMessage={disabledMessage}
projectReady={projectReady}
tunnelAvailable={tunnelAvailable}
creating={creating}
loading={loading}
error={error}
onCreateTerminal={createTerminal}
onStartTool={startToolTab}
onOpenBackgroundTasks={openBackgroundTasks}
/>
<div className="relative flex min-h-0 flex-1 flex-col">
{/* 桌面端不再浮动「+」——启动页卡片即完整的创建入口,终端卡
片自带 shell 选择;这里只保留移动端的关闭按钮。 */}
{onClose ? (
<Button
variant="ghost"
size="icon"
onClick={onClose}
title={t("projectTools.closePanel")}
className="absolute right-2 top-2 z-10 h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground md:hidden"
>
<X className="h-4 w-4" />
</Button>
) : null}
<RightDockChooser
shellOptions={shellOptions}
terminalReady={terminalReady}
terminalDisabledMessage={terminalDisabledMessage}
disabledMessage={disabledMessage}
projectReady={projectReady}
tunnelAvailable={tunnelAvailable}
creating={creating}
loading={loading}
error={error}
onCreateTerminal={createTerminal}
onStartTool={startToolTab}
onOpenBackgroundTasks={openBackgroundTasks}
/>
</div>
) : (
<RightDockContent
currentActiveTab={currentActiveTab}
Expand Down
Loading
Loading