@atercates/claude-deck 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useCallback,
|
|
8
|
+
useState,
|
|
9
|
+
useMemo,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { useTheme } from "next-themes";
|
|
12
|
+
import "@xterm/xterm/css/xterm.css";
|
|
13
|
+
import { Paperclip, WifiOff, Upload, Loader2 } from "lucide-react";
|
|
14
|
+
import { cn } from "@/lib/utils";
|
|
15
|
+
import { SearchBar } from "./SearchBar";
|
|
16
|
+
import { ScrollToBottomButton } from "./ScrollToBottomButton";
|
|
17
|
+
import { TerminalToolbar } from "./TerminalToolbar";
|
|
18
|
+
import { useTerminalConnection, useTerminalSearch } from "./hooks";
|
|
19
|
+
import type { TerminalScrollState } from "./hooks";
|
|
20
|
+
import { useViewport } from "@/hooks/useViewport";
|
|
21
|
+
import { useFileDrop } from "@/hooks/useFileDrop";
|
|
22
|
+
import { uploadFileToTemp } from "@/lib/file-upload";
|
|
23
|
+
import { FilePicker } from "@/components/FilePicker";
|
|
24
|
+
|
|
25
|
+
export type { TerminalScrollState };
|
|
26
|
+
|
|
27
|
+
export interface TerminalHandle {
|
|
28
|
+
sendCommand: (command: string) => void;
|
|
29
|
+
sendInput: (data: string) => void;
|
|
30
|
+
focus: () => void;
|
|
31
|
+
getScrollState: () => TerminalScrollState | null;
|
|
32
|
+
restoreScrollState: (state: TerminalScrollState) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TerminalProps {
|
|
36
|
+
onConnected?: () => void;
|
|
37
|
+
onDisconnected?: () => void;
|
|
38
|
+
onBeforeUnmount?: (scrollState: TerminalScrollState) => void;
|
|
39
|
+
initialScrollState?: TerminalScrollState;
|
|
40
|
+
/** Show image picker button (default: true) */
|
|
41
|
+
showImageButton?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Terminal = forwardRef<TerminalHandle, TerminalProps>(
|
|
45
|
+
function Terminal(
|
|
46
|
+
{
|
|
47
|
+
onConnected,
|
|
48
|
+
onDisconnected,
|
|
49
|
+
onBeforeUnmount,
|
|
50
|
+
initialScrollState,
|
|
51
|
+
showImageButton = true,
|
|
52
|
+
},
|
|
53
|
+
ref
|
|
54
|
+
) {
|
|
55
|
+
const terminalRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
57
|
+
const { isMobile } = useViewport();
|
|
58
|
+
const { theme: currentTheme, resolvedTheme } = useTheme();
|
|
59
|
+
const [showFilePicker, setShowFilePicker] = useState(false);
|
|
60
|
+
const [selectMode, setSelectMode] = useState(false);
|
|
61
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
62
|
+
|
|
63
|
+
// Use the full theme string (e.g., "dark-purple") for terminal theming
|
|
64
|
+
const terminalTheme = useMemo(() => {
|
|
65
|
+
// For system theme, use the resolved theme
|
|
66
|
+
if (currentTheme === "system") {
|
|
67
|
+
return resolvedTheme || "dark";
|
|
68
|
+
}
|
|
69
|
+
return currentTheme || "dark";
|
|
70
|
+
}, [currentTheme, resolvedTheme]);
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
connectionState,
|
|
74
|
+
isAtBottom,
|
|
75
|
+
xtermRef,
|
|
76
|
+
searchAddonRef,
|
|
77
|
+
scrollToBottom,
|
|
78
|
+
copySelection,
|
|
79
|
+
sendInput,
|
|
80
|
+
sendCommand,
|
|
81
|
+
focus,
|
|
82
|
+
getScrollState,
|
|
83
|
+
restoreScrollState,
|
|
84
|
+
reconnect,
|
|
85
|
+
} = useTerminalConnection({
|
|
86
|
+
terminalRef,
|
|
87
|
+
onConnected,
|
|
88
|
+
onDisconnected,
|
|
89
|
+
onBeforeUnmount,
|
|
90
|
+
initialScrollState,
|
|
91
|
+
isMobile,
|
|
92
|
+
theme: terminalTheme,
|
|
93
|
+
selectMode,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const {
|
|
97
|
+
searchVisible,
|
|
98
|
+
searchQuery,
|
|
99
|
+
setSearchQuery,
|
|
100
|
+
searchInputRef,
|
|
101
|
+
closeSearch,
|
|
102
|
+
findNext,
|
|
103
|
+
findPrevious,
|
|
104
|
+
} = useTerminalSearch(searchAddonRef, xtermRef);
|
|
105
|
+
|
|
106
|
+
// Handle image selection - paste file path into terminal
|
|
107
|
+
const handleImageSelect = useCallback(
|
|
108
|
+
(filePath: string) => {
|
|
109
|
+
sendInput(filePath);
|
|
110
|
+
setShowFilePicker(false);
|
|
111
|
+
focus();
|
|
112
|
+
},
|
|
113
|
+
[sendInput, focus]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Handle file drop - upload and insert path into terminal
|
|
117
|
+
const handleFileDrop = useCallback(
|
|
118
|
+
async (file: File) => {
|
|
119
|
+
setIsUploading(true);
|
|
120
|
+
try {
|
|
121
|
+
const path = await uploadFileToTemp(file);
|
|
122
|
+
if (path) {
|
|
123
|
+
sendInput(path);
|
|
124
|
+
focus();
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error("Failed to upload file:", err);
|
|
128
|
+
} finally {
|
|
129
|
+
setIsUploading(false);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[sendInput, focus]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Drag and drop for file uploads
|
|
136
|
+
const { isDragging, dragHandlers } = useFileDrop(
|
|
137
|
+
containerRef,
|
|
138
|
+
handleFileDrop,
|
|
139
|
+
{ disabled: isUploading || showFilePicker }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Expose imperative methods
|
|
143
|
+
useImperativeHandle(ref, () => ({
|
|
144
|
+
sendCommand,
|
|
145
|
+
sendInput,
|
|
146
|
+
focus,
|
|
147
|
+
getScrollState,
|
|
148
|
+
restoreScrollState,
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
// Extract terminal text for select mode overlay
|
|
152
|
+
const terminalText = useMemo(() => {
|
|
153
|
+
if (!selectMode || !xtermRef.current) return "";
|
|
154
|
+
|
|
155
|
+
const term = xtermRef.current;
|
|
156
|
+
const buffer = term.buffer.active;
|
|
157
|
+
const startRow = Math.max(0, buffer.baseY - 500);
|
|
158
|
+
const endRow = buffer.baseY + term.rows;
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
|
|
161
|
+
for (let i = startRow; i < endRow; i++) {
|
|
162
|
+
const line = buffer.getLine(i);
|
|
163
|
+
if (line) lines.push(line.translateToString(true));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}, [selectMode, xtermRef]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={containerRef}
|
|
172
|
+
className="bg-background flex flex-col overflow-hidden"
|
|
173
|
+
style={{
|
|
174
|
+
position: "relative",
|
|
175
|
+
width: "100%",
|
|
176
|
+
height: "100%",
|
|
177
|
+
}}
|
|
178
|
+
{...dragHandlers}
|
|
179
|
+
>
|
|
180
|
+
{/* Search Bar */}
|
|
181
|
+
<SearchBar
|
|
182
|
+
ref={searchInputRef}
|
|
183
|
+
visible={searchVisible}
|
|
184
|
+
query={searchQuery}
|
|
185
|
+
onQueryChange={setSearchQuery}
|
|
186
|
+
onFindNext={findNext}
|
|
187
|
+
onFindPrevious={findPrevious}
|
|
188
|
+
onClose={closeSearch}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{/* Terminal container - NO padding! FitAddon reads offsetHeight which includes padding */}
|
|
192
|
+
<div
|
|
193
|
+
ref={terminalRef}
|
|
194
|
+
className={cn(
|
|
195
|
+
"terminal-container min-h-0 w-full flex-1 overflow-hidden",
|
|
196
|
+
selectMode && "ring-primary ring-2 ring-inset",
|
|
197
|
+
isDragging && "ring-primary ring-2 ring-inset"
|
|
198
|
+
)}
|
|
199
|
+
onClick={focus}
|
|
200
|
+
onTouchStart={selectMode ? (e) => e.stopPropagation() : undefined}
|
|
201
|
+
onTouchEnd={selectMode ? (e) => e.stopPropagation() : undefined}
|
|
202
|
+
/>
|
|
203
|
+
|
|
204
|
+
{/* Select mode overlay - shows terminal text in a selectable format */}
|
|
205
|
+
{selectMode && (
|
|
206
|
+
<div
|
|
207
|
+
className="bg-background absolute inset-0 z-40 flex flex-col"
|
|
208
|
+
onTouchStart={(e) => e.stopPropagation()}
|
|
209
|
+
onTouchEnd={(e) => e.stopPropagation()}
|
|
210
|
+
>
|
|
211
|
+
<div className="bg-primary text-primary-foreground flex items-center justify-between px-3 py-2 text-xs font-medium">
|
|
212
|
+
<span>Select text below, then tap Copy</span>
|
|
213
|
+
<button
|
|
214
|
+
onClick={() => setSelectMode(false)}
|
|
215
|
+
className="bg-primary-foreground/20 rounded px-2 py-0.5 text-xs"
|
|
216
|
+
>
|
|
217
|
+
Done
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
<pre
|
|
221
|
+
className="flex-1 overflow-auto p-3 font-mono text-xs break-all whitespace-pre-wrap select-text"
|
|
222
|
+
style={{
|
|
223
|
+
userSelect: "text",
|
|
224
|
+
WebkitUserSelect: "text",
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{terminalText}
|
|
228
|
+
</pre>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Drag and drop overlay */}
|
|
233
|
+
{isDragging && (
|
|
234
|
+
<div className="bg-primary/10 pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
|
|
235
|
+
<div className="border-primary bg-background/90 rounded-lg border px-6 py-4 text-center shadow-lg">
|
|
236
|
+
<Upload className="text-primary mx-auto mb-2 h-8 w-8" />
|
|
237
|
+
<p className="text-sm font-medium">Drop file to upload</p>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Upload in progress overlay */}
|
|
243
|
+
{isUploading && (
|
|
244
|
+
<div className="bg-background/50 pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
|
|
245
|
+
<div className="bg-background rounded-lg border px-6 py-4 text-center shadow-lg">
|
|
246
|
+
<Loader2 className="text-primary mx-auto mb-2 h-6 w-6 animate-spin" />
|
|
247
|
+
<p className="text-sm">Uploading file...</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{/* File picker button - desktop only, for agent terminals */}
|
|
253
|
+
{!isMobile && showImageButton && (
|
|
254
|
+
<button
|
|
255
|
+
onClick={() => setShowFilePicker(true)}
|
|
256
|
+
className="bg-secondary hover:bg-accent absolute top-3 right-3 z-40 flex h-9 w-9 items-center justify-center rounded-full shadow-lg transition-all"
|
|
257
|
+
title="Attach file"
|
|
258
|
+
>
|
|
259
|
+
<Paperclip className="h-4 w-4" />
|
|
260
|
+
</button>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Image picker modal */}
|
|
264
|
+
{showFilePicker && (
|
|
265
|
+
<FilePicker
|
|
266
|
+
initialPath="~"
|
|
267
|
+
onSelect={handleImageSelect}
|
|
268
|
+
onClose={() => setShowFilePicker(false)}
|
|
269
|
+
/>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{/* Scroll to bottom button */}
|
|
273
|
+
<ScrollToBottomButton visible={!isAtBottom} onClick={scrollToBottom} />
|
|
274
|
+
|
|
275
|
+
{/* Mobile: Toolbar with special keys (native keyboard handles text) */}
|
|
276
|
+
{isMobile && (
|
|
277
|
+
<TerminalToolbar
|
|
278
|
+
onKeyPress={sendInput}
|
|
279
|
+
onFilePicker={() => setShowFilePicker(true)}
|
|
280
|
+
onCopy={copySelection}
|
|
281
|
+
selectMode={selectMode}
|
|
282
|
+
onSelectModeChange={setSelectMode}
|
|
283
|
+
visible={true}
|
|
284
|
+
/>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Connection status overlays */}
|
|
288
|
+
{connectionState === "connecting" && (
|
|
289
|
+
<div className="bg-background absolute inset-0 z-20 flex flex-col items-center justify-center gap-3">
|
|
290
|
+
<div className="bg-primary h-2 w-2 animate-pulse rounded-full" />
|
|
291
|
+
<span className="text-muted-foreground text-sm">Connecting...</span>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{connectionState === "reconnecting" && (
|
|
296
|
+
<div className="absolute top-4 left-4 flex items-center gap-2 rounded bg-amber-500/20 px-2 py-1 text-xs text-amber-400">
|
|
297
|
+
<div className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
|
|
298
|
+
Reconnecting...
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Disconnected overlay - shows tap to reconnect button */}
|
|
303
|
+
{connectionState === "disconnected" && (
|
|
304
|
+
<button
|
|
305
|
+
onClick={reconnect}
|
|
306
|
+
className="bg-background/80 active:bg-background/90 absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 backdrop-blur-sm transition-all"
|
|
307
|
+
>
|
|
308
|
+
<WifiOff className="text-muted-foreground h-8 w-8" />
|
|
309
|
+
<span className="text-foreground text-sm font-medium">
|
|
310
|
+
Connection lost
|
|
311
|
+
</span>
|
|
312
|
+
<span className="bg-primary text-primary-foreground rounded-full px-4 py-2 text-sm font-medium">
|
|
313
|
+
Tap to reconnect
|
|
314
|
+
</span>
|
|
315
|
+
</button>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Moon, Sun, Monitor, Check, Palette } from "lucide-react";
|
|
4
|
+
import { useTheme } from "next-themes";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuLabel,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
DropdownMenuSub,
|
|
14
|
+
DropdownMenuSubContent,
|
|
15
|
+
DropdownMenuSubTrigger,
|
|
16
|
+
} from "@/components/ui/dropdown-menu";
|
|
17
|
+
import {
|
|
18
|
+
DARK_THEMES,
|
|
19
|
+
LIGHT_THEMES,
|
|
20
|
+
parseTheme,
|
|
21
|
+
buildTheme,
|
|
22
|
+
type DarkThemeVariant,
|
|
23
|
+
type LightThemeVariant,
|
|
24
|
+
} from "@/lib/theme-config";
|
|
25
|
+
|
|
26
|
+
export function ThemeToggle() {
|
|
27
|
+
const { theme, setTheme } = useTheme();
|
|
28
|
+
const { mode, variant } = parseTheme(theme || "system");
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger asChild>
|
|
33
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
34
|
+
<Palette className="h-4 w-4" />
|
|
35
|
+
<span className="sr-only">Change theme</span>
|
|
36
|
+
</Button>
|
|
37
|
+
</DropdownMenuTrigger>
|
|
38
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
39
|
+
{/* Light Themes */}
|
|
40
|
+
<DropdownMenuSub>
|
|
41
|
+
<DropdownMenuSubTrigger>
|
|
42
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
43
|
+
<span>Light</span>
|
|
44
|
+
{mode === "light" && (
|
|
45
|
+
<Check className="text-primary ml-auto h-4 w-4" />
|
|
46
|
+
)}
|
|
47
|
+
</DropdownMenuSubTrigger>
|
|
48
|
+
<DropdownMenuSubContent className="max-h-[50vh] w-56 overflow-y-auto">
|
|
49
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
50
|
+
Choose your light theme
|
|
51
|
+
</DropdownMenuLabel>
|
|
52
|
+
<DropdownMenuSeparator />
|
|
53
|
+
{LIGHT_THEMES.map((lightTheme) => {
|
|
54
|
+
const Icon = lightTheme.icon;
|
|
55
|
+
const isActive = mode === "light" && variant === lightTheme.id;
|
|
56
|
+
return (
|
|
57
|
+
<DropdownMenuItem
|
|
58
|
+
key={lightTheme.id}
|
|
59
|
+
onClick={() =>
|
|
60
|
+
setTheme(
|
|
61
|
+
buildTheme("light", lightTheme.id as LightThemeVariant)
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
className="cursor-pointer"
|
|
65
|
+
>
|
|
66
|
+
<Icon className="mr-2 h-4 w-4 flex-shrink-0" />
|
|
67
|
+
<div className="flex flex-1 flex-col gap-0.5">
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<span className="text-sm font-medium">
|
|
70
|
+
{lightTheme.label}
|
|
71
|
+
</span>
|
|
72
|
+
{isActive && <Check className="text-primary h-4 w-4" />}
|
|
73
|
+
</div>
|
|
74
|
+
<span className="text-muted-foreground text-xs">
|
|
75
|
+
{lightTheme.description}
|
|
76
|
+
</span>
|
|
77
|
+
{/* Color preview */}
|
|
78
|
+
<div className="mt-1 flex gap-1">
|
|
79
|
+
<div
|
|
80
|
+
className="border-border/50 h-3 w-8 rounded-sm border"
|
|
81
|
+
style={{
|
|
82
|
+
backgroundColor: lightTheme.preview.background,
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
<div
|
|
86
|
+
className="border-border/50 h-3 w-8 rounded-sm border"
|
|
87
|
+
style={{ backgroundColor: lightTheme.preview.accent }}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</DropdownMenuItem>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</DropdownMenuSubContent>
|
|
95
|
+
</DropdownMenuSub>
|
|
96
|
+
|
|
97
|
+
{/* Dark Themes */}
|
|
98
|
+
<DropdownMenuSub>
|
|
99
|
+
<DropdownMenuSubTrigger>
|
|
100
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
101
|
+
<span>Dark</span>
|
|
102
|
+
{mode === "dark" && (
|
|
103
|
+
<Check className="text-primary ml-auto h-4 w-4" />
|
|
104
|
+
)}
|
|
105
|
+
</DropdownMenuSubTrigger>
|
|
106
|
+
<DropdownMenuSubContent className="max-h-[50vh] w-56 overflow-y-auto">
|
|
107
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
108
|
+
Choose your dark theme
|
|
109
|
+
</DropdownMenuLabel>
|
|
110
|
+
<DropdownMenuSeparator />
|
|
111
|
+
{DARK_THEMES.map((darkTheme) => {
|
|
112
|
+
const Icon = darkTheme.icon;
|
|
113
|
+
const isActive = mode === "dark" && variant === darkTheme.id;
|
|
114
|
+
return (
|
|
115
|
+
<DropdownMenuItem
|
|
116
|
+
key={darkTheme.id}
|
|
117
|
+
onClick={() =>
|
|
118
|
+
setTheme(
|
|
119
|
+
buildTheme("dark", darkTheme.id as DarkThemeVariant)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
className="cursor-pointer"
|
|
123
|
+
>
|
|
124
|
+
<Icon className="mr-2 h-4 w-4 flex-shrink-0" />
|
|
125
|
+
<div className="flex flex-1 flex-col gap-0.5">
|
|
126
|
+
<div className="flex items-center justify-between">
|
|
127
|
+
<span className="text-sm font-medium">
|
|
128
|
+
{darkTheme.label}
|
|
129
|
+
</span>
|
|
130
|
+
{isActive && <Check className="text-primary h-4 w-4" />}
|
|
131
|
+
</div>
|
|
132
|
+
<span className="text-muted-foreground text-xs">
|
|
133
|
+
{darkTheme.description}
|
|
134
|
+
</span>
|
|
135
|
+
{/* Color preview */}
|
|
136
|
+
<div className="mt-1 flex gap-1">
|
|
137
|
+
<div
|
|
138
|
+
className="border-border/50 h-3 w-8 rounded-sm border"
|
|
139
|
+
style={{
|
|
140
|
+
backgroundColor: darkTheme.preview.background,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
<div
|
|
144
|
+
className="border-border/50 h-3 w-8 rounded-sm border"
|
|
145
|
+
style={{ backgroundColor: darkTheme.preview.accent }}
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</DropdownMenuItem>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</DropdownMenuSubContent>
|
|
153
|
+
</DropdownMenuSub>
|
|
154
|
+
|
|
155
|
+
<DropdownMenuSeparator />
|
|
156
|
+
|
|
157
|
+
{/* System Theme */}
|
|
158
|
+
<DropdownMenuItem onClick={() => setTheme("system")}>
|
|
159
|
+
<Monitor className="mr-2 h-4 w-4" />
|
|
160
|
+
<span>System</span>
|
|
161
|
+
{mode === "system" && (
|
|
162
|
+
<Check className="text-primary ml-auto h-4 w-4" />
|
|
163
|
+
)}
|
|
164
|
+
</DropdownMenuItem>
|
|
165
|
+
</DropdownMenuContent>
|
|
166
|
+
</DropdownMenu>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { Button } from "./ui/button";
|
|
5
|
+
import { Badge } from "./ui/badge";
|
|
6
|
+
import { RefreshCw, Terminal, MonitorUp } from "lucide-react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
|
|
9
|
+
interface TmuxSession {
|
|
10
|
+
name: string;
|
|
11
|
+
windows: number;
|
|
12
|
+
created: string;
|
|
13
|
+
attached: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface TmuxSessionsProps {
|
|
17
|
+
onAttach: (sessionName: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TmuxSessions({ onAttach }: TmuxSessionsProps) {
|
|
21
|
+
const [sessions, setSessions] = useState<TmuxSession[]>([]);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
const fetchSessions = useCallback(async () => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch("/api/exec", {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
command:
|
|
34
|
+
"tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}' 2>/dev/null || echo ''",
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
|
|
39
|
+
if (data.success && data.output.trim()) {
|
|
40
|
+
const parsed = data.output
|
|
41
|
+
.trim()
|
|
42
|
+
.split("\n")
|
|
43
|
+
.filter((line: string) => line.includes("|"))
|
|
44
|
+
.map((line: string) => {
|
|
45
|
+
const [name, windows, created, attached] = line.split("|");
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
windows: parseInt(windows),
|
|
49
|
+
created: new Date(parseInt(created) * 1000).toLocaleString(),
|
|
50
|
+
attached: attached === "1",
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
setSessions(parsed);
|
|
54
|
+
} else {
|
|
55
|
+
setSessions([]);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error("Failed to fetch tmux sessions:", err);
|
|
59
|
+
setError("Failed to load");
|
|
60
|
+
setSessions([]);
|
|
61
|
+
} finally {
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
fetchSessions();
|
|
68
|
+
// Refresh every 30 seconds
|
|
69
|
+
const interval = setInterval(fetchSessions, 30000);
|
|
70
|
+
return () => clearInterval(interval);
|
|
71
|
+
}, [fetchSessions]);
|
|
72
|
+
|
|
73
|
+
if (sessions.length === 0 && !loading && !error) {
|
|
74
|
+
return null; // Don't show section if no tmux sessions
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="border-border border-b">
|
|
79
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
80
|
+
<div className="flex items-center gap-2">
|
|
81
|
+
<Terminal className="text-muted-foreground h-4 w-4" />
|
|
82
|
+
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
|
|
83
|
+
Tmux Sessions
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="icon-sm"
|
|
89
|
+
onClick={fetchSessions}
|
|
90
|
+
disabled={loading}
|
|
91
|
+
className="h-6 w-6"
|
|
92
|
+
>
|
|
93
|
+
<RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="space-y-1 px-4 pb-3">
|
|
98
|
+
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
99
|
+
{sessions.map((session) => (
|
|
100
|
+
<button
|
|
101
|
+
key={session.name}
|
|
102
|
+
onClick={() => onAttach(session.name)}
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex w-full items-center justify-between rounded-md p-2 text-left transition-colors",
|
|
105
|
+
"hover:bg-primary/10 border",
|
|
106
|
+
session.attached
|
|
107
|
+
? "border-primary/50 bg-primary/5"
|
|
108
|
+
: "border-transparent"
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
112
|
+
<MonitorUp className="text-primary h-4 w-4 flex-shrink-0" />
|
|
113
|
+
<span className="truncate text-sm font-medium">
|
|
114
|
+
{session.name}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex flex-shrink-0 items-center gap-2">
|
|
118
|
+
<span className="text-muted-foreground text-xs">
|
|
119
|
+
{session.windows}w
|
|
120
|
+
</span>
|
|
121
|
+
{session.attached && (
|
|
122
|
+
<Badge variant="success" className="px-1 py-0 text-[10px]">
|
|
123
|
+
attached
|
|
124
|
+
</Badge>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Wrench, Check, X, Loader2 } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface ToolCallDisplayProps {
|
|
7
|
+
name: string;
|
|
8
|
+
input: Record<string, unknown>;
|
|
9
|
+
output?: string;
|
|
10
|
+
status: "pending" | "running" | "completed" | "error";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ToolCallDisplay({
|
|
14
|
+
name,
|
|
15
|
+
input,
|
|
16
|
+
output,
|
|
17
|
+
status,
|
|
18
|
+
}: ToolCallDisplayProps) {
|
|
19
|
+
const StatusIcon = {
|
|
20
|
+
pending: Loader2,
|
|
21
|
+
running: Loader2,
|
|
22
|
+
completed: Check,
|
|
23
|
+
error: X,
|
|
24
|
+
}[status];
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="border-border my-2 ml-11 overflow-hidden rounded-lg border">
|
|
28
|
+
{/* Header */}
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
"flex items-center gap-2 px-3 py-2",
|
|
32
|
+
status === "error" ? "bg-destructive/10" : "bg-muted/50"
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<Wrench className="text-muted-foreground h-4 w-4" />
|
|
36
|
+
<span className="font-mono text-sm">{name}</span>
|
|
37
|
+
<StatusIcon
|
|
38
|
+
className={cn(
|
|
39
|
+
"ml-auto h-4 w-4",
|
|
40
|
+
status === "running" && "animate-spin text-yellow-400",
|
|
41
|
+
status === "pending" && "text-muted-foreground animate-spin",
|
|
42
|
+
status === "completed" && "text-primary",
|
|
43
|
+
status === "error" && "text-destructive"
|
|
44
|
+
)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Input */}
|
|
49
|
+
<details className="group">
|
|
50
|
+
<summary className="text-muted-foreground hover:text-foreground cursor-pointer px-3 py-1 text-xs">
|
|
51
|
+
Input
|
|
52
|
+
</summary>
|
|
53
|
+
<pre className="text-muted-foreground bg-background overflow-x-auto px-3 py-2 text-xs">
|
|
54
|
+
{JSON.stringify(input, null, 2)}
|
|
55
|
+
</pre>
|
|
56
|
+
</details>
|
|
57
|
+
|
|
58
|
+
{/* Output */}
|
|
59
|
+
{output && (
|
|
60
|
+
<details className="group" open={status === "completed"}>
|
|
61
|
+
<summary className="text-muted-foreground hover:text-foreground cursor-pointer px-3 py-1 text-xs">
|
|
62
|
+
Output
|
|
63
|
+
</summary>
|
|
64
|
+
<pre className="text-muted-foreground bg-background max-h-48 overflow-x-auto px-3 py-2 text-xs">
|
|
65
|
+
{output.length > 1000 ? output.slice(0, 1000) + "..." : output}
|
|
66
|
+
</pre>
|
|
67
|
+
</details>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|