增加文件管理器
This commit is contained in:
12
bun.lock
12
bun.lock
@@ -5,6 +5,8 @@
|
||||
"name": "claudia",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
@@ -224,6 +226,10 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@monaco-editor/loader": ["@monaco-editor/loader@1.5.0", "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.5.0.tgz", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw=="],
|
||||
|
||||
"@monaco-editor/react": ["@monaco-editor/react@4.7.0", "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.7.0.tgz", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
@@ -264,6 +270,8 @@
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.15", "https://registry.npmmirror.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsQUMjcYTsBjTSXw0P3GO0werEQvUY2plgRQuKoCTtkNr45q1DiL51j4m7gxhABzZ0BadoXNsIbg7F3KwiUBbw=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||
@@ -854,6 +862,8 @@
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"monaco-editor": ["monaco-editor@0.52.2", "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.52.2.tgz", {}, "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.18.1", "", { "dependencies": { "motion-utils": "^12.18.1" } }, "sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.18.1", "", {}, "sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA=="],
|
||||
@@ -970,6 +980,8 @@
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"state-local": ["state-local@1.0.7", "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"style-to-js": ["style-to-js@1.1.17", "", { "dependencies": { "style-to-object": "1.0.9" } }, "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA=="],
|
||||
|
@@ -13,6 +13,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
|
@@ -9,3 +9,5 @@ pub mod language;
|
||||
pub mod relay_stations;
|
||||
pub mod relay_adapters;
|
||||
pub mod packycode_nodes;
|
||||
pub mod filesystem;
|
||||
pub mod git;
|
||||
|
@@ -60,6 +60,13 @@ use commands::relay_adapters::{
|
||||
use commands::packycode_nodes::{
|
||||
test_all_packycode_nodes, auto_select_best_node, get_packycode_nodes,
|
||||
};
|
||||
use commands::filesystem::{
|
||||
read_directory_tree, search_files_by_name, get_file_info, watch_directory,
|
||||
read_file, write_file,
|
||||
};
|
||||
use commands::git::{
|
||||
get_git_status, get_git_history, get_git_branches, get_git_diff,
|
||||
};
|
||||
use process::ProcessRegistryState;
|
||||
use std::sync::Mutex;
|
||||
use tauri::Manager;
|
||||
@@ -296,6 +303,20 @@ fn main() {
|
||||
test_all_packycode_nodes,
|
||||
auto_select_best_node,
|
||||
get_packycode_nodes,
|
||||
|
||||
// File System
|
||||
read_directory_tree,
|
||||
search_files_by_name,
|
||||
get_file_info,
|
||||
watch_directory,
|
||||
read_file,
|
||||
write_file,
|
||||
|
||||
// Git
|
||||
get_git_status,
|
||||
get_git_history,
|
||||
get_git_branches,
|
||||
get_git_diff,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
@@ -33,6 +33,9 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { SplitPane } from "@/components/ui/split-pane";
|
||||
import { WebviewPreview } from "./WebviewPreview";
|
||||
import { FileExplorerPanel } from "./FileExplorerPanel";
|
||||
import { GitPanel } from "./GitPanel";
|
||||
import { FileEditor } from "./FileEditor";
|
||||
import type { ClaudeStreamMessage } from "./AgentExecution";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useTrackEvent, useComponentMetrics, useWorkflowTracking } from "@/hooks";
|
||||
@@ -110,6 +113,15 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
// Add collapsed state for queued prompts
|
||||
const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);
|
||||
|
||||
// New state for file explorer and git panel
|
||||
const [showFileExplorer, setShowFileExplorer] = useState(false);
|
||||
const [showGitPanel, setShowGitPanel] = useState(false);
|
||||
const [fileExplorerWidth] = useState(280);
|
||||
const [gitPanelWidth] = useState(320);
|
||||
|
||||
// File editor state
|
||||
const [editingFile, setEditingFile] = useState<string | null>(null);
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const unlistenRefs = useRef<UnlistenFn[]>([]);
|
||||
const hasActiveSessionRef = useRef(false);
|
||||
@@ -438,7 +450,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
// If already loading, queue the prompt
|
||||
if (isLoading) {
|
||||
const newPrompt = {
|
||||
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
||||
prompt,
|
||||
model
|
||||
};
|
||||
@@ -1022,7 +1034,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const newSessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newSessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
await api.forkFromCheckpoint(
|
||||
forkCheckpointId,
|
||||
effectiveSession.id,
|
||||
@@ -1280,6 +1292,48 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* File Explorer Toggle */}
|
||||
{projectPath && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowFileExplorer(!showFileExplorer)}
|
||||
className={cn("h-8 w-8", showFileExplorer && "text-primary")}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>File Explorer</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Git Panel Toggle */}
|
||||
{projectPath && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setShowGitPanel(!showGitPanel)}
|
||||
className={cn("h-8 w-8", showGitPanel && "text-primary")}
|
||||
>
|
||||
<GitBranch className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Git Panel</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{projectPath && onProjectSettings && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -1352,7 +1406,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Timeline Navigator</p>
|
||||
<p>{t('app.timeline')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -1398,11 +1452,33 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
{/* Main Content Area with panels */}
|
||||
<div className={cn(
|
||||
"flex-1 overflow-hidden transition-all duration-300",
|
||||
"flex-1 overflow-hidden transition-all duration-300 flex",
|
||||
showTimeline && "sm:mr-96"
|
||||
)}>
|
||||
{/* File Explorer Panel */}
|
||||
<FileExplorerPanel
|
||||
projectPath={projectPath}
|
||||
isVisible={showFileExplorer}
|
||||
onFileSelect={(path) => {
|
||||
// Add file path to prompt input (double click)
|
||||
floatingPromptRef.current?.addImage(path);
|
||||
}}
|
||||
onFileOpen={(path) => {
|
||||
// Open file in editor (single click)
|
||||
setEditingFile(path);
|
||||
}}
|
||||
onToggle={() => setShowFileExplorer(!showFileExplorer)}
|
||||
width={fileExplorerWidth}
|
||||
/>
|
||||
|
||||
{/* Main Content with Input */}
|
||||
<div className={cn(
|
||||
"flex-1 transition-all duration-300 relative flex flex-col",
|
||||
showFileExplorer && "pl-[280px]",
|
||||
showGitPanel && "pr-[320px]"
|
||||
)}>
|
||||
{showPreview ? (
|
||||
// Split pane layout when preview is active
|
||||
<SplitPane
|
||||
@@ -1427,182 +1503,198 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
minRightWidth={400}
|
||||
className="h-full"
|
||||
/>
|
||||
) : editingFile ? (
|
||||
// File Editor layout
|
||||
<div className="h-full flex flex-col relative">
|
||||
<FileEditor
|
||||
filePath={editingFile}
|
||||
onClose={() => setEditingFile(null)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// Original layout when no preview
|
||||
<div className="h-full flex flex-col max-w-5xl mx-auto">
|
||||
{projectPathInput}
|
||||
{messagesList}
|
||||
|
||||
{isLoading && messages.length === 0 && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rotating-symbol text-primary" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{session ? "Loading session history..." : "Initializing Claude Code..."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Floating Prompt Input - Always visible */}
|
||||
<ErrorBoundary>
|
||||
{/* Queued Prompts Display */}
|
||||
<AnimatePresence>
|
||||
{queuedPrompts.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
className="fixed bottom-24 left-1/2 -translate-x-1/2 z-30 w-full max-w-3xl px-4"
|
||||
>
|
||||
<div className="bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||
Queued Prompts ({queuedPrompts.length})
|
||||
<div className="h-full flex flex-col relative">
|
||||
<div className="flex-1 flex flex-col max-w-5xl mx-auto w-full">
|
||||
{projectPathInput}
|
||||
{messagesList}
|
||||
|
||||
{isLoading && messages.length === 0 && (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rotating-symbol text-primary" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{session ? "Loading session history..." : "Initializing Claude Code..."}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={() => setQueuedPromptsCollapsed(prev => !prev)}>
|
||||
{queuedPromptsCollapsed ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
{!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => (
|
||||
<motion.div
|
||||
key={queuedPrompt.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="flex items-start gap-2 bg-muted/50 rounded-md p-2"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">#{index + 1}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">
|
||||
{queuedPrompt.model === "opus" ? "Opus" : "Sonnet"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm line-clamp-2 break-words">{queuedPrompt.prompt}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
onClick={() => setQueuedPrompts(prev => prev.filter(p => p.id !== queuedPrompt.id))}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Navigation Arrows - positioned above prompt bar with spacing */}
|
||||
{displayableMessages.length > 5 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="fixed bottom-32 right-6 z-50"
|
||||
>
|
||||
<div className="flex items-center bg-background/95 backdrop-blur-md border rounded-full shadow-lg overflow-hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Use virtualizer to scroll to the first item
|
||||
if (displayableMessages.length > 0) {
|
||||
// Scroll to top of the container
|
||||
parentRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// After smooth scroll completes, trigger a small scroll to ensure rendering
|
||||
setTimeout(() => {
|
||||
if (parentRef.current) {
|
||||
// Scroll down 1px then back to 0 to trigger virtualizer update
|
||||
parentRef.current.scrollTop = 1;
|
||||
requestAnimationFrame(() => {
|
||||
if (parentRef.current) {
|
||||
parentRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500); // Wait for smooth scroll to complete
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||
title="Scroll to top"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-border" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Use virtualizer to scroll to the last item
|
||||
if (displayableMessages.length > 0) {
|
||||
// Scroll to bottom of the container
|
||||
const scrollElement = parentRef.current;
|
||||
if (scrollElement) {
|
||||
scrollElement.scrollTo({
|
||||
top: scrollElement.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Floating Prompt Input - Bound to Main Content */}
|
||||
<ErrorBoundary>
|
||||
{/* Queued Prompts Display */}
|
||||
<AnimatePresence>
|
||||
{queuedPrompts.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
className={cn(
|
||||
"absolute bottom-24 left-0 right-0 z-30 transition-all duration-300",
|
||||
showTimeline && "sm:right-96"
|
||||
)}
|
||||
>
|
||||
<div className="mx-4">
|
||||
<div className="bg-background/95 backdrop-blur-md border rounded-lg shadow-lg p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1">
|
||||
Queued Prompts ({queuedPrompts.length})
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" onClick={() => setQueuedPromptsCollapsed(prev => !prev)}>
|
||||
{queuedPromptsCollapsed ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
{!queuedPromptsCollapsed && queuedPrompts.map((queuedPrompt, index) => (
|
||||
<motion.div
|
||||
key={queuedPrompt.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ delay: index * 0.05 }}
|
||||
className="flex items-start gap-2 bg-muted/50 rounded-md p-2"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">#{index + 1}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">
|
||||
{queuedPrompt.model === "opus" ? "Opus" : "Sonnet"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm line-clamp-2 break-words">{queuedPrompt.prompt}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 flex-shrink-0"
|
||||
onClick={() => setQueuedPrompts(prev => prev.filter(p => p.id !== queuedPrompt.id))}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className={cn(
|
||||
"absolute bottom-0 left-0 right-0 transition-all duration-300 z-50",
|
||||
showTimeline && "sm:right-96"
|
||||
)}>
|
||||
<FloatingPromptInput
|
||||
ref={floatingPromptRef}
|
||||
onSend={handleSendPrompt}
|
||||
onCancel={handleCancelExecution}
|
||||
isLoading={isLoading}
|
||||
disabled={!projectPath}
|
||||
projectPath={projectPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Token Counter - positioned under the Send button */}
|
||||
{totalTokens > 0 && (
|
||||
<div className="fixed bottom-0 left-0 right-0 z-30 pointer-events-none">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex justify-end px-4 pb-2">
|
||||
{/* Navigation Arrows */}
|
||||
{displayableMessages.length > 5 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="bg-background/95 backdrop-blur-md border rounded-full px-3 py-1 shadow-lg pointer-events-auto"
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute bottom-32 right-6 z-50"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<Hash className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-mono">{totalTokens.toLocaleString()}</span>
|
||||
<span className="text-muted-foreground">tokens</span>
|
||||
<div className="flex items-center bg-background/95 backdrop-blur-md border rounded-full shadow-lg overflow-hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (displayableMessages.length > 0) {
|
||||
parentRef.current?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (parentRef.current) {
|
||||
parentRef.current.scrollTop = 1;
|
||||
requestAnimationFrame(() => {
|
||||
if (parentRef.current) {
|
||||
parentRef.current.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||
title="Scroll to top"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="w-px h-4 bg-border" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (displayableMessages.length > 0) {
|
||||
const scrollElement = parentRef.current;
|
||||
if (scrollElement) {
|
||||
scrollElement.scrollTo({
|
||||
top: scrollElement.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="px-3 py-2 hover:bg-accent rounded-none"
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 z-50">
|
||||
<FloatingPromptInput
|
||||
ref={floatingPromptRef}
|
||||
onSend={handleSendPrompt}
|
||||
onCancel={handleCancelExecution}
|
||||
isLoading={isLoading}
|
||||
disabled={!projectPath}
|
||||
projectPath={projectPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Counter */}
|
||||
{totalTokens > 0 && (
|
||||
<div className="absolute bottom-0 right-0 z-30 pointer-events-none">
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end px-4 pb-2">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="bg-background/95 backdrop-blur-md border rounded-full px-3 py-1 shadow-lg pointer-events-auto"
|
||||
>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<Hash className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-mono">{totalTokens.toLocaleString()}</span>
|
||||
<span className="text-muted-foreground">tokens</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* Git Panel */}
|
||||
<GitPanel
|
||||
projectPath={projectPath}
|
||||
isVisible={showGitPanel}
|
||||
onToggle={() => setShowGitPanel(!showGitPanel)}
|
||||
width={gitPanelWidth}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
<AnimatePresence>
|
||||
@@ -1665,7 +1757,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
placeholder="e.g., Alternative approach"
|
||||
value={forkSessionName}
|
||||
onChange={(e) => setForkSessionName(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !isLoading) {
|
||||
handleConfirmFork();
|
||||
}
|
||||
@@ -1731,3 +1823,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Add default export for lazy loading
|
||||
export default ClaudeCodeSession;
|
||||
|
@@ -12,7 +12,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
// Lazy load heavy components
|
||||
const ClaudeCodeSession = lazy(() => import('@/components/ClaudeCodeSession').then(m => ({ default: m.ClaudeCodeSession })));
|
||||
const ClaudeCodeSession = lazy(() => import('./ClaudeCodeSession'));
|
||||
const AgentRunOutputViewer = lazy(() => import('@/components/AgentRunOutputViewer'));
|
||||
const AgentExecution = lazy(() => import('@/components/AgentExecution').then(m => ({ default: m.AgentExecution })));
|
||||
const CreateAgent = lazy(() => import('@/components/CreateAgent').then(m => ({ default: m.CreateAgent })));
|
||||
|
@@ -61,7 +61,38 @@
|
||||
"noCheckpointsYet": "No checkpoints yet",
|
||||
"sessionTimeline": "Session Timeline",
|
||||
"checkpoints": "checkpoints",
|
||||
"loadingTimeline": "Loading timeline..."
|
||||
"loadingTimeline": "Loading timeline...",
|
||||
"fileExplorer": "File Explorer",
|
||||
"searchFiles": "Search files...",
|
||||
"copyPath": "Copy Path",
|
||||
"openFile": "Open File",
|
||||
"addToChat": "Add to Chat",
|
||||
"noFilesFound": "No files found",
|
||||
"viewMode": "View Mode",
|
||||
"editMode": "Edit Mode",
|
||||
"saved": "Saved",
|
||||
"unsavedChangesConfirm": "You have unsaved changes. Are you sure you want to leave?",
|
||||
"gitPanel": "Git",
|
||||
"gitStatus": "Status",
|
||||
"gitHistory": "History",
|
||||
"gitBranches": "Branches",
|
||||
"workingTreeClean": "Working tree clean",
|
||||
"staged": "Staged",
|
||||
"modified": "Modified",
|
||||
"untracked": "Untracked",
|
||||
"conflicted": "Conflicted",
|
||||
"noGitRepository": "No Git repository found",
|
||||
"noCommitsFound": "No commits found",
|
||||
"noBranchesFound": "No branches found",
|
||||
"localBranches": "Local Branches",
|
||||
"remoteBranches": "Remote Branches",
|
||||
"current": "current",
|
||||
"andMore": "... and {{count}} more",
|
||||
"filesChanged": "files",
|
||||
"justNow": "just now",
|
||||
"minutesAgo": "{{count}} minute{{plural}} ago",
|
||||
"hoursAgo": "{{count}} hour{{plural}} ago",
|
||||
"daysAgo": "{{count}} day{{plural}} ago"
|
||||
},
|
||||
"navigation": {
|
||||
"projects": "CC Projects",
|
||||
|
@@ -58,7 +58,38 @@
|
||||
"noCheckpointsYet": "尚无检查点",
|
||||
"sessionTimeline": "会话时间线",
|
||||
"checkpoints": "个检查点",
|
||||
"loadingTimeline": "加载时间线中..."
|
||||
"loadingTimeline": "加载时间线中...",
|
||||
"fileExplorer": "文件管理器",
|
||||
"searchFiles": "搜索文件...",
|
||||
"copyPath": "复制路径",
|
||||
"openFile": "打开文件",
|
||||
"addToChat": "添加到聊天",
|
||||
"noFilesFound": "未找到文件",
|
||||
"viewMode": "查看模式",
|
||||
"editMode": "编辑模式",
|
||||
"saved": "已保存",
|
||||
"unsavedChangesConfirm": "您有未保存的更改。确定要离开吗?",
|
||||
"gitPanel": "Git",
|
||||
"gitStatus": "状态",
|
||||
"gitHistory": "历史",
|
||||
"gitBranches": "分支",
|
||||
"workingTreeClean": "工作区清洁",
|
||||
"staged": "已暂存",
|
||||
"modified": "已修改",
|
||||
"untracked": "未跟踪",
|
||||
"conflicted": "冲突",
|
||||
"noGitRepository": "未找到Git仓库",
|
||||
"noCommitsFound": "未找到提交",
|
||||
"noBranchesFound": "未找到分支",
|
||||
"localBranches": "本地分支",
|
||||
"remoteBranches": "远程分支",
|
||||
"current": "当前",
|
||||
"andMore": "... 还有 {{count}} 个",
|
||||
"filesChanged": "个文件",
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "{{count}} 分钟前",
|
||||
"hoursAgo": "{{count}} 小时前",
|
||||
"daysAgo": "{{count}} 天前"
|
||||
},
|
||||
"navigation": {
|
||||
"projects": "Claude Code 项目",
|
||||
|
Reference in New Issue
Block a user