Initial commit
This commit is contained in:
122
src/app/commit/page.tsx
Normal file
122
src/app/commit/page.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
"use client"
|
||||
|
||||
import { Suspense, useCallback, useEffect, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { CommitWorkspace } from "@/components/layout/commit-dialog"
|
||||
import { AppTitleBar } from "@/components/layout/app-title-bar"
|
||||
import { AppToaster } from "@/components/ui/app-toaster"
|
||||
import { getFolder } from "@/lib/tauri"
|
||||
import type { FolderDetail } from "@/lib/types"
|
||||
|
||||
const TOAST_DURATION_MS = 6000
|
||||
|
||||
interface FolderLoadState {
|
||||
loadedId: number | null
|
||||
folder: FolderDetail | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
function CommitPageInner() {
|
||||
const searchParams = useSearchParams()
|
||||
const [state, setState] = useState<FolderLoadState>({
|
||||
loadedId: null,
|
||||
folder: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const folderId = Number(searchParams.get("folderId") ?? "0")
|
||||
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
|
||||
const hasValidFolderId = normalizedFolderId > 0
|
||||
const loading = hasValidFolderId && state.loadedId !== normalizedFolderId
|
||||
const folder = state.loadedId === normalizedFolderId ? state.folder : null
|
||||
const error = state.loadedId === normalizedFolderId ? state.error : null
|
||||
|
||||
const closeWindow = useCallback(() => {
|
||||
getCurrentWindow()
|
||||
.close()
|
||||
.catch((err) => {
|
||||
console.error("[CommitPage] failed to close window:", err)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasValidFolderId) return
|
||||
|
||||
let cancelled = false
|
||||
|
||||
getFolder(normalizedFolderId)
|
||||
.then((detail) => {
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
loadedId: normalizedFolderId,
|
||||
folder: detail,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!cancelled) {
|
||||
setState({
|
||||
loadedId: normalizedFolderId,
|
||||
folder: null,
|
||||
error: String(err),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [hasValidFolderId, normalizedFolderId])
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<AppTitleBar
|
||||
center={
|
||||
<div className="text-sm font-semibold tracking-tight">
|
||||
Git Commit{hasValidFolderId && folder ? ` · ${folder.name}` : ""}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<main className="flex-1 min-h-0 p-3">
|
||||
{!hasValidFolderId ? (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
缺少有效的 folderId 参数
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
正在加载仓库信息...
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
) : folder ? (
|
||||
<CommitWorkspace
|
||||
folderPath={folder.path}
|
||||
onCommitted={closeWindow}
|
||||
onCancel={closeWindow}
|
||||
/>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<AppToaster
|
||||
position="bottom-right"
|
||||
duration={TOAST_DURATION_MS}
|
||||
closeButton
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CommitPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<CommitPageInner />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
693
src/app/folder/layout.tsx
Normal file
693
src/app/folder/layout.tsx
Normal file
@@ -0,0 +1,693 @@
|
||||
"use client"
|
||||
|
||||
import { Suspense, useCallback, useEffect, useRef, useState } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import type { ImperativePanelGroupHandle } from "react-resizable-panels"
|
||||
import { FolderTitleBar } from "@/components/layout/folder-title-bar"
|
||||
import { Sidebar } from "@/components/layout/sidebar"
|
||||
import { StatusBar } from "@/components/layout/status-bar"
|
||||
import { FolderProvider } from "@/contexts/folder-context"
|
||||
import { TaskProvider } from "@/contexts/task-context"
|
||||
import { AlertProvider } from "@/contexts/alert-context"
|
||||
import { AcpConnectionsProvider } from "@/contexts/acp-connections-context"
|
||||
import { TabProvider } from "@/contexts/tab-context"
|
||||
import { SessionStatsProvider } from "@/contexts/session-stats-context"
|
||||
import { SidebarProvider, useSidebarContext } from "@/contexts/sidebar-context"
|
||||
import {
|
||||
AuxPanelProvider,
|
||||
useAuxPanelContext,
|
||||
} from "@/contexts/aux-panel-context"
|
||||
import {
|
||||
TerminalProvider,
|
||||
useTerminalContext,
|
||||
} from "@/contexts/terminal-context"
|
||||
import {
|
||||
WorkspaceProvider,
|
||||
useWorkspaceContext,
|
||||
} from "@/contexts/workspace-context"
|
||||
import { TabBar } from "@/components/tabs/tab-bar"
|
||||
import { TerminalPanel } from "@/components/terminal/terminal-panel"
|
||||
import { AuxPanel } from "@/components/layout/aux-panel"
|
||||
import { FileWorkspaceTabBar } from "@/components/files/file-workspace-tab-bar"
|
||||
import { FileWorkspacePanel } from "@/components/files/file-workspace-panel"
|
||||
import { AppToaster } from "@/components/ui/app-toaster"
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable"
|
||||
import type { AgentType } from "@/lib/types"
|
||||
|
||||
const TOAST_DURATION_MS = 15000
|
||||
const WORKSPACE_PANEL_GROUP_ID = "workspace-panel-group"
|
||||
const WORKSPACE_CONVERSATION_PANEL_ID = "workspace-conversation-panel"
|
||||
const WORKSPACE_FILES_PANEL_ID = "workspace-files-panel"
|
||||
const FOLDER_SHELL_GROUP_ID = "folder-shell-group"
|
||||
const FOLDER_SHELL_LEFT_PANEL_ID = "folder-shell-left-panel"
|
||||
const FOLDER_SHELL_MAIN_PANEL_ID = "folder-shell-main-panel"
|
||||
const FOLDER_SHELL_RIGHT_PANEL_ID = "folder-shell-right-panel"
|
||||
const FOLDER_MAIN_GROUP_ID = "folder-main-group"
|
||||
const FOLDER_MAIN_WORKSPACE_PANEL_ID = "folder-main-workspace-panel"
|
||||
const FOLDER_MAIN_TERMINAL_PANEL_ID = "folder-main-terminal-panel"
|
||||
const CONVERSATION_ONLY_LAYOUT: [number, number] = [100, 0]
|
||||
const FILES_ONLY_LAYOUT: [number, number] = [0, 100]
|
||||
const DEFAULT_FUSION_LAYOUT: [number, number] = [56, 44]
|
||||
const MIN_CENTER_WIDTH_PX = 420
|
||||
const MIN_WORKSPACE_HEIGHT_PX = 220
|
||||
const LAYOUT_EPSILON = 0.25
|
||||
|
||||
function isSameLayout(a: number[], b: number[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
return a.every((value, index) => Math.abs(value - b[index]) <= LAYOUT_EPSILON)
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
function toPercent(pixels: number, totalPixels: number): number {
|
||||
if (totalPixels <= 0) return 0
|
||||
return (pixels / totalPixels) * 100
|
||||
}
|
||||
|
||||
function resolvePanelSizeRange(
|
||||
minPixels: number,
|
||||
maxPixels: number,
|
||||
totalPixels: number
|
||||
): { minSize: number; maxSize: number } {
|
||||
const safeTotal = totalPixels > 0 ? totalPixels : 1
|
||||
const minSize = clamp(toPercent(minPixels, safeTotal), 0, 100)
|
||||
const maxSize = clamp(toPercent(maxPixels, safeTotal), minSize, 100)
|
||||
return { minSize, maxSize }
|
||||
}
|
||||
|
||||
function WorkspaceContent({ children }: { children: React.ReactNode }) {
|
||||
const { mode, setActivePane } = useWorkspaceContext()
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle | null>(null)
|
||||
const fusionLayoutRef = useRef<[number, number]>(DEFAULT_FUSION_LAYOUT)
|
||||
const desiredLayoutRef = useRef<[number, number]>(DEFAULT_FUSION_LAYOUT)
|
||||
const appliedLayoutRef = useRef<[number, number] | null>(null)
|
||||
|
||||
const markConversationActive = useCallback(() => {
|
||||
if (mode !== "fusion") return
|
||||
setActivePane("conversation")
|
||||
}, [mode, setActivePane])
|
||||
|
||||
const markFileActive = useCallback(() => {
|
||||
if (mode !== "fusion") return
|
||||
setActivePane("files")
|
||||
}, [mode, setActivePane])
|
||||
|
||||
const applyLayout = useCallback((layout: [number, number]) => {
|
||||
desiredLayoutRef.current = layout
|
||||
if (
|
||||
appliedLayoutRef.current &&
|
||||
isSameLayout(appliedLayoutRef.current, layout)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const panelGroup = panelGroupRef.current
|
||||
if (!panelGroup) return
|
||||
|
||||
try {
|
||||
panelGroup.setLayout(layout)
|
||||
appliedLayoutRef.current = layout
|
||||
} catch {
|
||||
// The group can be transiently unavailable while registering panels.
|
||||
// onLayout will retry once registration completes.
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "fusion") {
|
||||
applyLayout(fusionLayoutRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === "conversation") {
|
||||
applyLayout(CONVERSATION_ONLY_LAYOUT)
|
||||
return
|
||||
}
|
||||
|
||||
applyLayout(FILES_ONLY_LAYOUT)
|
||||
}, [applyLayout, mode])
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(layout: number[]) => {
|
||||
if (layout.length !== 2) return
|
||||
|
||||
const normalizedLayout: [number, number] = [layout[0], layout[1]]
|
||||
appliedLayoutRef.current = normalizedLayout
|
||||
|
||||
const desired = desiredLayoutRef.current
|
||||
if (mode !== "fusion" && !isSameLayout(normalizedLayout, desired)) {
|
||||
applyLayout(desired)
|
||||
return
|
||||
}
|
||||
|
||||
if (mode !== "fusion") return
|
||||
|
||||
const [conversationSize, fileSize] = normalizedLayout
|
||||
if (conversationSize <= 0 || fileSize <= 0) return
|
||||
fusionLayoutRef.current = [conversationSize, fileSize]
|
||||
},
|
||||
[applyLayout, mode]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
<ResizablePanelGroup
|
||||
id={WORKSPACE_PANEL_GROUP_ID}
|
||||
ref={panelGroupRef}
|
||||
direction="horizontal"
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
<ResizablePanel
|
||||
id={WORKSPACE_CONVERSATION_PANEL_ID}
|
||||
order={1}
|
||||
defaultSize={56}
|
||||
minSize={mode === "fusion" ? 25 : 0}
|
||||
>
|
||||
<section
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden"
|
||||
onPointerDownCapture={markConversationActive}
|
||||
onFocusCapture={markConversationActive}
|
||||
aria-hidden={mode === "files"}
|
||||
>
|
||||
<TabBar />
|
||||
<div className="relative flex-1 min-h-0 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
withHandle
|
||||
className={
|
||||
mode === "fusion"
|
||||
? ""
|
||||
: "pointer-events-none w-0 opacity-0 after:w-0"
|
||||
}
|
||||
/>
|
||||
<ResizablePanel
|
||||
id={WORKSPACE_FILES_PANEL_ID}
|
||||
order={2}
|
||||
defaultSize={44}
|
||||
minSize={mode === "fusion" ? 20 : 0}
|
||||
>
|
||||
<section
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden"
|
||||
onPointerDownCapture={markFileActive}
|
||||
onFocusCapture={markFileActive}
|
||||
aria-hidden={mode === "conversation"}
|
||||
>
|
||||
<FileWorkspaceTabBar />
|
||||
<div className="flex-1 min-h-0 overflow-hidden">
|
||||
<FileWorkspacePanel />
|
||||
</div>
|
||||
</section>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderWorkspaceShell({ children }: { children: React.ReactNode }) {
|
||||
const {
|
||||
isOpen: sidebarOpen,
|
||||
width: sidebarWidth,
|
||||
minWidth: sidebarMinWidth,
|
||||
maxWidth: sidebarMaxWidth,
|
||||
setWidth: setSidebarWidth,
|
||||
} = useSidebarContext()
|
||||
const {
|
||||
isOpen: auxOpen,
|
||||
width: auxWidth,
|
||||
minWidth: auxMinWidth,
|
||||
maxWidth: auxMaxWidth,
|
||||
setWidth: setAuxWidth,
|
||||
} = useAuxPanelContext()
|
||||
const {
|
||||
isOpen: terminalOpen,
|
||||
height: terminalHeight,
|
||||
minHeight: terminalMinHeight,
|
||||
maxHeight: terminalMaxHeight,
|
||||
setHeight: setTerminalHeight,
|
||||
} = useTerminalContext()
|
||||
|
||||
const shellGroupRef = useRef<ImperativePanelGroupHandle | null>(null)
|
||||
const mainGroupRef = useRef<ImperativePanelGroupHandle | null>(null)
|
||||
const shellContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const mainContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const [shellWidth, setShellWidth] = useState(0)
|
||||
const [mainHeight, setMainHeight] = useState(0)
|
||||
|
||||
const shellDesiredLayoutRef = useRef<[number, number, number]>([0, 100, 0])
|
||||
const shellAppliedLayoutRef = useRef<[number, number, number] | null>(null)
|
||||
const mainDesiredLayoutRef = useRef<[number, number]>([100, 0])
|
||||
const mainAppliedLayoutRef = useRef<[number, number] | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const container = shellContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
const updateWidth = (next: number) => {
|
||||
setShellWidth((prev) => (Math.abs(prev - next) < 1 ? prev : next))
|
||||
}
|
||||
|
||||
updateWidth(container.clientWidth)
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
updateWidth(entries[0]?.contentRect.width ?? container.clientWidth)
|
||||
})
|
||||
|
||||
observer.observe(container)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const container = mainContainerRef.current
|
||||
if (!container) return
|
||||
|
||||
const updateHeight = (next: number) => {
|
||||
setMainHeight((prev) => (Math.abs(prev - next) < 1 ? prev : next))
|
||||
}
|
||||
|
||||
updateHeight(container.clientHeight)
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
updateHeight(entries[0]?.contentRect.height ?? container.clientHeight)
|
||||
})
|
||||
|
||||
observer.observe(container)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buildShellLayout = useCallback((): [number, number, number] => {
|
||||
const requestedLeft = sidebarOpen
|
||||
? clamp(sidebarWidth, sidebarMinWidth, sidebarMaxWidth)
|
||||
: 0
|
||||
const requestedRight = auxOpen
|
||||
? clamp(auxWidth, auxMinWidth, auxMaxWidth)
|
||||
: 0
|
||||
|
||||
const totalWidth =
|
||||
shellWidth > 0 ? shellWidth : requestedLeft + requestedRight + 960
|
||||
|
||||
let left = requestedLeft
|
||||
let right = requestedRight
|
||||
|
||||
const maxSideTotal = Math.max(0, totalWidth - MIN_CENTER_WIDTH_PX)
|
||||
const sideTotal = left + right
|
||||
if (sideTotal > maxSideTotal && sideTotal > 0) {
|
||||
const scale = maxSideTotal / sideTotal
|
||||
left *= scale
|
||||
right *= scale
|
||||
}
|
||||
|
||||
const center = Math.max(1, totalWidth - left - right)
|
||||
const total = left + center + right
|
||||
|
||||
return [(left / total) * 100, (center / total) * 100, (right / total) * 100]
|
||||
}, [
|
||||
auxMaxWidth,
|
||||
auxMinWidth,
|
||||
auxOpen,
|
||||
auxWidth,
|
||||
shellWidth,
|
||||
sidebarMaxWidth,
|
||||
sidebarMinWidth,
|
||||
sidebarOpen,
|
||||
sidebarWidth,
|
||||
])
|
||||
|
||||
const buildMainLayout = useCallback((): [number, number] => {
|
||||
if (!terminalOpen) {
|
||||
return [100, 0]
|
||||
}
|
||||
|
||||
const requestedTerminalHeight = clamp(
|
||||
terminalHeight,
|
||||
terminalMinHeight,
|
||||
terminalMaxHeight
|
||||
)
|
||||
const totalHeight =
|
||||
mainHeight > 0 ? mainHeight : requestedTerminalHeight + 640
|
||||
|
||||
const maxTerminalHeight = Math.max(0, totalHeight - MIN_WORKSPACE_HEIGHT_PX)
|
||||
const terminal = Math.min(requestedTerminalHeight, maxTerminalHeight)
|
||||
const workspace = Math.max(1, totalHeight - terminal)
|
||||
const total = workspace + terminal
|
||||
|
||||
return [(workspace / total) * 100, (terminal / total) * 100]
|
||||
}, [
|
||||
mainHeight,
|
||||
terminalHeight,
|
||||
terminalMaxHeight,
|
||||
terminalMinHeight,
|
||||
terminalOpen,
|
||||
])
|
||||
|
||||
const applyShellLayout = useCallback((layout: [number, number, number]) => {
|
||||
shellDesiredLayoutRef.current = layout
|
||||
if (
|
||||
shellAppliedLayoutRef.current &&
|
||||
isSameLayout(shellAppliedLayoutRef.current, layout)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const shellGroup = shellGroupRef.current
|
||||
if (!shellGroup) return
|
||||
|
||||
try {
|
||||
shellGroup.setLayout(layout)
|
||||
shellAppliedLayoutRef.current = layout
|
||||
} catch {
|
||||
// The group can be transiently unavailable while registering panels.
|
||||
// onLayout will retry once registration completes.
|
||||
}
|
||||
}, [])
|
||||
|
||||
const applyMainLayout = useCallback((layout: [number, number]) => {
|
||||
mainDesiredLayoutRef.current = layout
|
||||
if (
|
||||
mainAppliedLayoutRef.current &&
|
||||
isSameLayout(mainAppliedLayoutRef.current, layout)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const mainGroup = mainGroupRef.current
|
||||
if (!mainGroup) return
|
||||
|
||||
try {
|
||||
mainGroup.setLayout(layout)
|
||||
mainAppliedLayoutRef.current = layout
|
||||
} catch {
|
||||
// The group can be transiently unavailable while registering panels.
|
||||
// onLayout will retry once registration completes.
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
applyShellLayout(buildShellLayout())
|
||||
}, [applyShellLayout, buildShellLayout])
|
||||
|
||||
useEffect(() => {
|
||||
applyMainLayout(buildMainLayout())
|
||||
}, [applyMainLayout, buildMainLayout])
|
||||
|
||||
const handleShellLayout = useCallback(
|
||||
(layout: number[]) => {
|
||||
if (layout.length !== 3) return
|
||||
|
||||
const normalizedLayout: [number, number, number] = [
|
||||
layout[0],
|
||||
layout[1],
|
||||
layout[2],
|
||||
]
|
||||
shellAppliedLayoutRef.current = normalizedLayout
|
||||
|
||||
const desired = shellDesiredLayoutRef.current
|
||||
const shouldEnforceDesiredLayout =
|
||||
(!sidebarOpen && normalizedLayout[0] > LAYOUT_EPSILON) ||
|
||||
(!auxOpen && normalizedLayout[2] > LAYOUT_EPSILON)
|
||||
|
||||
if (
|
||||
shouldEnforceDesiredLayout &&
|
||||
!isSameLayout(normalizedLayout, desired)
|
||||
) {
|
||||
applyShellLayout(desired)
|
||||
return
|
||||
}
|
||||
|
||||
if (shellWidth <= 0) return
|
||||
|
||||
if (sidebarOpen) {
|
||||
const nextSidebarWidth = (normalizedLayout[0] / 100) * shellWidth
|
||||
const withinSidebarRange =
|
||||
nextSidebarWidth >= sidebarMinWidth - 1 &&
|
||||
nextSidebarWidth <= sidebarMaxWidth + 1
|
||||
if (
|
||||
withinSidebarRange &&
|
||||
Math.abs(nextSidebarWidth - sidebarWidth) >= 1
|
||||
) {
|
||||
setSidebarWidth(nextSidebarWidth)
|
||||
}
|
||||
}
|
||||
|
||||
if (auxOpen) {
|
||||
const nextAuxWidth = (normalizedLayout[2] / 100) * shellWidth
|
||||
const withinAuxRange =
|
||||
nextAuxWidth >= auxMinWidth - 1 && nextAuxWidth <= auxMaxWidth + 1
|
||||
if (withinAuxRange && Math.abs(nextAuxWidth - auxWidth) >= 1) {
|
||||
setAuxWidth(nextAuxWidth)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
applyShellLayout,
|
||||
auxMaxWidth,
|
||||
auxMinWidth,
|
||||
auxOpen,
|
||||
auxWidth,
|
||||
setAuxWidth,
|
||||
setSidebarWidth,
|
||||
shellWidth,
|
||||
sidebarMaxWidth,
|
||||
sidebarMinWidth,
|
||||
sidebarOpen,
|
||||
sidebarWidth,
|
||||
]
|
||||
)
|
||||
|
||||
const handleMainLayout = useCallback(
|
||||
(layout: number[]) => {
|
||||
if (layout.length !== 2) return
|
||||
|
||||
const normalizedLayout: [number, number] = [layout[0], layout[1]]
|
||||
mainAppliedLayoutRef.current = normalizedLayout
|
||||
|
||||
const desired = mainDesiredLayoutRef.current
|
||||
if (
|
||||
!terminalOpen &&
|
||||
normalizedLayout[1] > LAYOUT_EPSILON &&
|
||||
!isSameLayout(normalizedLayout, desired)
|
||||
) {
|
||||
applyMainLayout(desired)
|
||||
return
|
||||
}
|
||||
|
||||
if (!terminalOpen || mainHeight <= 0) return
|
||||
|
||||
const nextTerminalHeight = (normalizedLayout[1] / 100) * mainHeight
|
||||
const withinTerminalRange =
|
||||
nextTerminalHeight >= terminalMinHeight - 1 &&
|
||||
nextTerminalHeight <= terminalMaxHeight + 1
|
||||
if (
|
||||
withinTerminalRange &&
|
||||
Math.abs(nextTerminalHeight - terminalHeight) >= 1
|
||||
) {
|
||||
setTerminalHeight(nextTerminalHeight)
|
||||
}
|
||||
},
|
||||
[
|
||||
applyMainLayout,
|
||||
mainHeight,
|
||||
setTerminalHeight,
|
||||
terminalHeight,
|
||||
terminalMaxHeight,
|
||||
terminalMinHeight,
|
||||
terminalOpen,
|
||||
]
|
||||
)
|
||||
|
||||
const safeShellWidth = shellWidth > 0 ? shellWidth : 1440
|
||||
const sidebarSizeRange = resolvePanelSizeRange(
|
||||
sidebarMinWidth,
|
||||
sidebarMaxWidth,
|
||||
safeShellWidth
|
||||
)
|
||||
const auxSizeRange = resolvePanelSizeRange(
|
||||
auxMinWidth,
|
||||
auxMaxWidth,
|
||||
safeShellWidth
|
||||
)
|
||||
|
||||
const safeMainHeight = mainHeight > 0 ? mainHeight : 900
|
||||
const terminalSizeRange = resolvePanelSizeRange(
|
||||
terminalMinHeight,
|
||||
terminalMaxHeight,
|
||||
safeMainHeight
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={shellContainerRef}
|
||||
className="flex flex-1 min-h-0 overflow-hidden"
|
||||
>
|
||||
<ResizablePanelGroup
|
||||
id={FOLDER_SHELL_GROUP_ID}
|
||||
ref={shellGroupRef}
|
||||
direction="horizontal"
|
||||
onLayout={handleShellLayout}
|
||||
>
|
||||
<ResizablePanel
|
||||
id={FOLDER_SHELL_LEFT_PANEL_ID}
|
||||
order={1}
|
||||
defaultSize={18}
|
||||
minSize={sidebarOpen ? sidebarSizeRange.minSize : 0}
|
||||
maxSize={sidebarOpen ? sidebarSizeRange.maxSize : 0}
|
||||
>
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle
|
||||
withHandle
|
||||
className={
|
||||
sidebarOpen ? "" : "pointer-events-none w-0 opacity-0 after:w-0"
|
||||
}
|
||||
/>
|
||||
|
||||
<ResizablePanel
|
||||
id={FOLDER_SHELL_MAIN_PANEL_ID}
|
||||
order={2}
|
||||
defaultSize={64}
|
||||
minSize={10}
|
||||
>
|
||||
<main
|
||||
ref={mainContainerRef}
|
||||
className="flex h-full min-h-0 flex-col overflow-hidden"
|
||||
>
|
||||
<ResizablePanelGroup
|
||||
id={FOLDER_MAIN_GROUP_ID}
|
||||
ref={mainGroupRef}
|
||||
direction="vertical"
|
||||
onLayout={handleMainLayout}
|
||||
>
|
||||
<ResizablePanel
|
||||
id={FOLDER_MAIN_WORKSPACE_PANEL_ID}
|
||||
order={1}
|
||||
defaultSize={72}
|
||||
minSize={15}
|
||||
>
|
||||
<WorkspaceContent>{children}</WorkspaceContent>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle
|
||||
withHandle
|
||||
className={
|
||||
terminalOpen
|
||||
? ""
|
||||
: "pointer-events-none h-0 opacity-0 after:h-0"
|
||||
}
|
||||
/>
|
||||
|
||||
<ResizablePanel
|
||||
id={FOLDER_MAIN_TERMINAL_PANEL_ID}
|
||||
order={2}
|
||||
defaultSize={28}
|
||||
minSize={terminalOpen ? terminalSizeRange.minSize : 0}
|
||||
maxSize={terminalOpen ? terminalSizeRange.maxSize : 0}
|
||||
>
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
<TerminalPanel />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle
|
||||
withHandle
|
||||
className={
|
||||
auxOpen ? "" : "pointer-events-none w-0 opacity-0 after:w-0"
|
||||
}
|
||||
/>
|
||||
|
||||
<ResizablePanel
|
||||
id={FOLDER_SHELL_RIGHT_PANEL_ID}
|
||||
order={3}
|
||||
defaultSize={18}
|
||||
minSize={auxOpen ? auxSizeRange.minSize : 0}
|
||||
maxSize={auxOpen ? auxSizeRange.maxSize : 0}
|
||||
>
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
<AuxPanel />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderLayoutInner({ children }: { children: React.ReactNode }) {
|
||||
const searchParams = useSearchParams()
|
||||
const folderId = Number(searchParams.get("id") ?? "0")
|
||||
const normalizedFolderId = Number.isFinite(folderId) ? folderId : 0
|
||||
const conversationId = searchParams.get("conversationId")
|
||||
const agentType = searchParams.get("agent") as AgentType | null
|
||||
|
||||
return (
|
||||
<FolderProvider
|
||||
folderId={normalizedFolderId}
|
||||
initialConversationId={conversationId ? Number(conversationId) : null}
|
||||
initialAgentType={agentType}
|
||||
>
|
||||
<AlertProvider>
|
||||
<TaskProvider>
|
||||
<AcpConnectionsProvider>
|
||||
<WorkspaceProvider key={`workspace-${normalizedFolderId}`}>
|
||||
<TabProvider>
|
||||
<SessionStatsProvider>
|
||||
<SidebarProvider
|
||||
key={`left-sidebar-${normalizedFolderId}`}
|
||||
folderId={normalizedFolderId}
|
||||
>
|
||||
<AuxPanelProvider
|
||||
key={`right-sidebar-${normalizedFolderId}`}
|
||||
folderId={normalizedFolderId}
|
||||
>
|
||||
<TerminalProvider>
|
||||
<div className="flex h-screen flex-col overflow-hidden">
|
||||
<FolderTitleBar />
|
||||
<FolderWorkspaceShell>
|
||||
{children}
|
||||
</FolderWorkspaceShell>
|
||||
<StatusBar />
|
||||
<AppToaster
|
||||
position="bottom-right"
|
||||
duration={TOAST_DURATION_MS}
|
||||
closeButton
|
||||
/>
|
||||
</div>
|
||||
</TerminalProvider>
|
||||
</AuxPanelProvider>
|
||||
</SidebarProvider>
|
||||
</SessionStatsProvider>
|
||||
</TabProvider>
|
||||
</WorkspaceProvider>
|
||||
</AcpConnectionsProvider>
|
||||
</TaskProvider>
|
||||
</AlertProvider>
|
||||
</FolderProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FolderLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Suspense>
|
||||
<FolderLayoutInner>{children}</FolderLayoutInner>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
7
src/app/folder/page.tsx
Normal file
7
src/app/folder/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { ConversationDetailPanel } from "@/components/conversations/conversation-detail-panel"
|
||||
|
||||
export default function FolderPage() {
|
||||
return <ConversationDetailPanel />
|
||||
}
|
||||
349
src/app/globals.css
Normal file
349
src/app/globals.css
Normal file
@@ -0,0 +1,349 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.58 0.22 27);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.87 0.00 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.87 0.00 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.371 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-sans);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Shiki dual-theme: switch to dark CSS variables in dark mode */
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) .shiki,
|
||||
:root:not(.light) .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Streamdown table overrides */
|
||||
[data-streamdown="table-wrapper"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Toolbar: float over table top-right, no vertical space */
|
||||
[data-streamdown="table-wrapper"] > div:first-child:has(button) {
|
||||
position: absolute;
|
||||
top: -2rem;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Streamdown code line number width by total line digits */
|
||||
[data-streamdown="code-block-body"] code {
|
||||
--sd-line-number-width: 1ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code:has(> span:nth-child(n + 10)) {
|
||||
--sd-line-number-width: 2ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code:has(> span:nth-child(n + 100)) {
|
||||
--sd-line-number-width: 3ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code:has(> span:nth-child(n + 1000)) {
|
||||
--sd-line-number-width: 4ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code:has(> span:nth-child(n + 10000)) {
|
||||
--sd-line-number-width: 5ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code:has(> span:nth-child(n + 100000)) {
|
||||
--sd-line-number-width: 6ch;
|
||||
}
|
||||
|
||||
[data-streamdown="code-block-body"] code > span::before {
|
||||
width: var(--sd-line-number-width) !important;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
|
||||
.select-none {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Monaco widgets: align popups/menus with shadcn surface style */
|
||||
.context-view .monaco-menu,
|
||||
.monaco-editor .suggest-widget,
|
||||
.monaco-editor .editor-widget,
|
||||
.monaco-editor .peekview-widget,
|
||||
.monaco-hover.workbench-hover {
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: 0 18px 36px -24px rgb(0 0 0 / 55%) !important;
|
||||
}
|
||||
|
||||
.context-view .monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item,
|
||||
.monaco-editor .suggest-widget .monaco-list-row,
|
||||
.monaco-editor .peekview-widget .monaco-list-row {
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-dirty-diff-glyph {
|
||||
margin-left: 5px;
|
||||
position: relative;
|
||||
border-left-style: solid;
|
||||
border-left-width: 2px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-dirty-diff-added {
|
||||
border-left-color: #2ea043;
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-dirty-diff-modified {
|
||||
border-left-color: #1f6feb;
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-dirty-diff-deleted {
|
||||
border-left-color: #cf222e;
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-dirty-diff-deleted::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border: 4px solid transparent;
|
||||
border-top-color: #cf222e;
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-session-diff-line-added {
|
||||
background-color: rgba(46, 160, 67, 0.14);
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-session-diff-line-modified {
|
||||
background-color: rgba(31, 111, 235, 0.12);
|
||||
}
|
||||
|
||||
.monaco-editor .codeg-session-diff-line-deleted {
|
||||
background-color: rgba(207, 34, 46, 0.12);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-dirty-diff-added {
|
||||
border-left-color: #3fb950;
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-dirty-diff-modified {
|
||||
border-left-color: #388bfd;
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-dirty-diff-deleted {
|
||||
border-left-color: #f85149;
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-dirty-diff-deleted::after {
|
||||
border-top-color: #f85149;
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-session-diff-line-added {
|
||||
background-color: rgba(63, 185, 80, 0.18);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-session-diff-line-modified {
|
||||
background-color: rgba(56, 139, 253, 0.2);
|
||||
}
|
||||
|
||||
.dark .monaco-editor .codeg-session-diff-line-deleted {
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not(.light) .monaco-editor .codeg-dirty-diff-added {
|
||||
border-left-color: #3fb950;
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-dirty-diff-modified {
|
||||
border-left-color: #388bfd;
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-dirty-diff-deleted {
|
||||
border-left-color: #f85149;
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-dirty-diff-deleted::after {
|
||||
border-top-color: #f85149;
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-session-diff-line-added {
|
||||
background-color: rgba(63, 185, 80, 0.18);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-session-diff-line-modified {
|
||||
background-color: rgba(56, 139, 253, 0.2);
|
||||
}
|
||||
|
||||
:root:not(.light) .monaco-editor .codeg-session-diff-line-deleted {
|
||||
background-color: rgba(248, 81, 73, 0.2);
|
||||
}
|
||||
}
|
||||
35
src/app/layout.tsx
Normal file
35
src/app/layout.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { Metadata } from "next"
|
||||
import "./globals.css"
|
||||
import { JetBrains_Mono } from "next/font/google"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "codeg",
|
||||
description: "AI Coding Agent Conversation Manager",
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={jetbrainsMono.variable} suppressHydrationWarning>
|
||||
<body>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
12
src/app/page.tsx
Normal file
12
src/app/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function Page() {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
router.replace("/welcome")
|
||||
}, [router])
|
||||
return null
|
||||
}
|
||||
16
src/app/settings/agents/page.tsx
Normal file
16
src/app/settings/agents/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Suspense } from "react"
|
||||
import { AcpAgentSettings } from "@/components/settings/acp-agent-settings"
|
||||
|
||||
export default function SettingsAgentsPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
|
||||
加载 Agent 设置中...
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AcpAgentSettings />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
5
src/app/settings/appearance/page.tsx
Normal file
5
src/app/settings/appearance/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { AppearanceSettings } from "@/components/settings/appearance-settings"
|
||||
|
||||
export default function SettingsAppearancePage() {
|
||||
return <AppearanceSettings />
|
||||
}
|
||||
9
src/app/settings/layout.tsx
Normal file
9
src/app/settings/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { SettingsShell } from "@/components/settings/settings-shell"
|
||||
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <SettingsShell>{children}</SettingsShell>
|
||||
}
|
||||
5
src/app/settings/mcp/page.tsx
Normal file
5
src/app/settings/mcp/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { McpSettings } from "@/components/settings/mcp-settings"
|
||||
|
||||
export default function SettingsMcpPage() {
|
||||
return <McpSettings />
|
||||
}
|
||||
14
src/app/settings/page.tsx
Normal file
14
src/app/settings/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
router.replace("/settings/appearance")
|
||||
}, [router])
|
||||
|
||||
return null
|
||||
}
|
||||
5
src/app/settings/shortcuts/page.tsx
Normal file
5
src/app/settings/shortcuts/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ShortcutSettings } from "@/components/settings/shortcut-settings"
|
||||
|
||||
export default function SettingsShortcutsPage() {
|
||||
return <ShortcutSettings />
|
||||
}
|
||||
5
src/app/settings/skills/page.tsx
Normal file
5
src/app/settings/skills/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SkillsSettings } from "@/components/settings/skills-settings"
|
||||
|
||||
export default function SettingsSkillsPage() {
|
||||
return <SkillsSettings />
|
||||
}
|
||||
5
src/app/settings/system/page.tsx
Normal file
5
src/app/settings/system/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SystemNetworkSettings } from "@/components/settings/system-network-settings"
|
||||
|
||||
export default function SettingsSystemPage() {
|
||||
return <SystemNetworkSettings />
|
||||
}
|
||||
7
src/app/welcome/page.tsx
Normal file
7
src/app/welcome/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { WelcomeScreen } from "@/components/welcome/welcome-screen"
|
||||
|
||||
export default function WelcomePage() {
|
||||
return <WelcomeScreen />
|
||||
}
|
||||
Reference in New Issue
Block a user