@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
package/app/page.tsx
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
// Debug log buffer - persists even if console is closed
|
|
6
|
+
const debugLogs: string[] = [];
|
|
7
|
+
const MAX_DEBUG_LOGS = 100;
|
|
8
|
+
|
|
9
|
+
function debugLog(message: string) {
|
|
10
|
+
const timestamp = new Date().toISOString().split("T")[1].slice(0, 12);
|
|
11
|
+
const entry = `[${timestamp}] ${message}`;
|
|
12
|
+
debugLogs.push(entry);
|
|
13
|
+
if (debugLogs.length > MAX_DEBUG_LOGS) debugLogs.shift();
|
|
14
|
+
console.log(`[ClaudeDeck] ${message}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Expose to window for debugging
|
|
18
|
+
if (typeof window !== "undefined") {
|
|
19
|
+
(window as unknown as { agentOSLogs: () => void }).agentOSLogs = () => {
|
|
20
|
+
console.log("=== ClaudeDeck Debug Logs ===");
|
|
21
|
+
debugLogs.forEach((log) => console.log(log));
|
|
22
|
+
console.log("=== End Logs ===");
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
import { PaneProvider, usePanes } from "@/contexts/PaneContext";
|
|
26
|
+
import { Pane } from "@/components/Pane";
|
|
27
|
+
import { useNotifications } from "@/hooks/useNotifications";
|
|
28
|
+
import { useViewport } from "@/hooks/useViewport";
|
|
29
|
+
import { useViewportHeight } from "@/hooks/useViewportHeight";
|
|
30
|
+
import { useSessions } from "@/hooks/useSessions";
|
|
31
|
+
import { useProjects } from "@/hooks/useProjects";
|
|
32
|
+
import { useDevServersManager } from "@/hooks/useDevServersManager";
|
|
33
|
+
import { useSessionStatuses } from "@/hooks/useSessionStatuses";
|
|
34
|
+
import type { Session } from "@/lib/db";
|
|
35
|
+
import type { TerminalHandle } from "@/components/Terminal";
|
|
36
|
+
import { getProvider } from "@/lib/providers";
|
|
37
|
+
import { DesktopView } from "@/components/views/DesktopView";
|
|
38
|
+
import { MobileView } from "@/components/views/MobileView";
|
|
39
|
+
import { getPendingPrompt, clearPendingPrompt } from "@/stores/initialPrompt";
|
|
40
|
+
import { NewClaudeSessionDialog } from "@/components/NewClaudeSessionDialog";
|
|
41
|
+
import { useClaudeProjectsQuery } from "@/data/claude";
|
|
42
|
+
|
|
43
|
+
function HomeContent() {
|
|
44
|
+
// UI State
|
|
45
|
+
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
46
|
+
const [showNewSessionDialog, setShowNewSessionDialog] = useState(false);
|
|
47
|
+
const [newSessionProjectId, setNewSessionProjectId] = useState<string | null>(
|
|
48
|
+
null
|
|
49
|
+
);
|
|
50
|
+
const [showNotificationSettings, setShowNotificationSettings] =
|
|
51
|
+
useState(false);
|
|
52
|
+
const [showQuickSwitcher, setShowQuickSwitcher] = useState(false);
|
|
53
|
+
const [copiedSessionId, setCopiedSessionId] = useState(false);
|
|
54
|
+
const terminalRefs = useRef<Map<string, TerminalHandle>>(new Map());
|
|
55
|
+
|
|
56
|
+
// Pane context
|
|
57
|
+
const { focusedPaneId, attachSession, getActiveTab, addTab } = usePanes();
|
|
58
|
+
const focusedActiveTab = getActiveTab(focusedPaneId);
|
|
59
|
+
const { isMobile, isHydrated } = useViewport();
|
|
60
|
+
|
|
61
|
+
// Data hooks
|
|
62
|
+
const { sessions, fetchSessions } = useSessions();
|
|
63
|
+
const { projects, fetchProjects } = useProjects();
|
|
64
|
+
const { data: claudeProjects } = useClaudeProjectsQuery();
|
|
65
|
+
const {
|
|
66
|
+
startDevServerProjectId,
|
|
67
|
+
setStartDevServerProjectId,
|
|
68
|
+
startDevServer,
|
|
69
|
+
createDevServer,
|
|
70
|
+
} = useDevServersManager();
|
|
71
|
+
|
|
72
|
+
// Helper to get init script command from API
|
|
73
|
+
const getInitScriptCommand = useCallback(
|
|
74
|
+
async (agentCommand: string): Promise<string> => {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch("/api/sessions/init-script", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/json" },
|
|
79
|
+
body: JSON.stringify({ agentCommand }),
|
|
80
|
+
});
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
return data.command || agentCommand;
|
|
83
|
+
} catch {
|
|
84
|
+
return agentCommand;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Set CSS variable for viewport height (handles mobile keyboard)
|
|
91
|
+
useViewportHeight();
|
|
92
|
+
|
|
93
|
+
// Terminal ref management
|
|
94
|
+
const registerTerminalRef = useCallback(
|
|
95
|
+
(paneId: string, tabId: string, ref: TerminalHandle | null) => {
|
|
96
|
+
const key = `${paneId}:${tabId}`;
|
|
97
|
+
if (ref) {
|
|
98
|
+
terminalRefs.current.set(key, ref);
|
|
99
|
+
debugLog(
|
|
100
|
+
`Terminal registered: ${key}, total refs: ${terminalRefs.current.size}`
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
terminalRefs.current.delete(key);
|
|
104
|
+
debugLog(
|
|
105
|
+
`Terminal unregistered: ${key}, total refs: ${terminalRefs.current.size}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
[]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Get terminal for a pane, with fallback to first available
|
|
113
|
+
const getTerminalWithFallback = useCallback(():
|
|
114
|
+
| { terminal: TerminalHandle; paneId: string; tabId: string }
|
|
115
|
+
| undefined => {
|
|
116
|
+
debugLog(
|
|
117
|
+
`getTerminalWithFallback called, total refs: ${terminalRefs.current.size}, focusedPaneId: ${focusedPaneId}`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Try focused pane first
|
|
121
|
+
const activeTab = getActiveTab(focusedPaneId);
|
|
122
|
+
debugLog(`activeTab for focused pane: ${activeTab?.id || "null"}`);
|
|
123
|
+
|
|
124
|
+
if (activeTab) {
|
|
125
|
+
const key = `${focusedPaneId}:${activeTab.id}`;
|
|
126
|
+
const terminal = terminalRefs.current.get(key);
|
|
127
|
+
debugLog(
|
|
128
|
+
`Looking for terminal at key "${key}": ${terminal ? "found" : "not found"}`
|
|
129
|
+
);
|
|
130
|
+
if (terminal) {
|
|
131
|
+
return { terminal, paneId: focusedPaneId, tabId: activeTab.id };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Fallback to first available terminal
|
|
136
|
+
const firstEntry = terminalRefs.current.entries().next().value;
|
|
137
|
+
if (firstEntry) {
|
|
138
|
+
const [key, terminal] = firstEntry as [string, TerminalHandle];
|
|
139
|
+
const [paneId, tabId] = key.split(":");
|
|
140
|
+
debugLog(`Using fallback terminal: ${key}`);
|
|
141
|
+
return { terminal, paneId, tabId };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
debugLog(
|
|
145
|
+
`NO TERMINAL FOUND. Available keys: ${Array.from(terminalRefs.current.keys()).join(", ") || "none"}`
|
|
146
|
+
);
|
|
147
|
+
return undefined;
|
|
148
|
+
}, [focusedPaneId, getActiveTab]);
|
|
149
|
+
|
|
150
|
+
// Build tmux command for a session
|
|
151
|
+
const buildSessionCommand = useCallback(
|
|
152
|
+
async (
|
|
153
|
+
session: Session
|
|
154
|
+
): Promise<{ sessionName: string; cwd: string; command: string }> => {
|
|
155
|
+
const provider = getProvider(session.agent_type || "claude");
|
|
156
|
+
const sessionName = session.tmux_name || `${provider.id}-${session.id}`;
|
|
157
|
+
const cwd = session.working_directory?.replace("~", "$HOME") || "$HOME";
|
|
158
|
+
|
|
159
|
+
// TODO: Add explicit "Enable Orchestration" toggle that creates .mcp.json
|
|
160
|
+
// for conductor sessions. Removed auto-creation because it pollutes projects
|
|
161
|
+
// with .mcp.json files that aren't in their .gitignore.
|
|
162
|
+
// See: /api/sessions/[id]/mcp-config, lib/mcp-config.ts
|
|
163
|
+
|
|
164
|
+
// Get parent session ID for forking
|
|
165
|
+
let parentSessionId: string | null = null;
|
|
166
|
+
if (!session.claude_session_id && session.parent_session_id) {
|
|
167
|
+
const parentSession = sessions.find(
|
|
168
|
+
(s) => s.id === session.parent_session_id
|
|
169
|
+
);
|
|
170
|
+
parentSessionId = parentSession?.claude_session_id || null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for pending initial prompt
|
|
174
|
+
const initialPrompt = getPendingPrompt(session.id);
|
|
175
|
+
if (initialPrompt) {
|
|
176
|
+
clearPendingPrompt(session.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const flags = provider.buildFlags({
|
|
180
|
+
sessionId: session.claude_session_id,
|
|
181
|
+
parentSessionId,
|
|
182
|
+
autoApprove: session.auto_approve,
|
|
183
|
+
model: session.model,
|
|
184
|
+
initialPrompt: initialPrompt || undefined,
|
|
185
|
+
});
|
|
186
|
+
const flagsStr = flags.join(" ");
|
|
187
|
+
|
|
188
|
+
const agentCmd = `${provider.command} ${flagsStr}`;
|
|
189
|
+
const command = await getInitScriptCommand(agentCmd);
|
|
190
|
+
|
|
191
|
+
return { sessionName, cwd, command };
|
|
192
|
+
},
|
|
193
|
+
[sessions, getInitScriptCommand]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Attach a session to a terminal
|
|
197
|
+
const runSessionInTerminal = useCallback(
|
|
198
|
+
(
|
|
199
|
+
terminal: TerminalHandle,
|
|
200
|
+
paneId: string,
|
|
201
|
+
session: Session,
|
|
202
|
+
sessionInfo: { sessionName: string; cwd: string; command: string }
|
|
203
|
+
) => {
|
|
204
|
+
const { sessionName, cwd, command } = sessionInfo;
|
|
205
|
+
const tmuxNew = command
|
|
206
|
+
? `tmux new -s ${sessionName} -c "${cwd}" "${command}"`
|
|
207
|
+
: `tmux new -s ${sessionName} -c "${cwd}"`;
|
|
208
|
+
terminal.sendCommand(
|
|
209
|
+
`tmux set -g mouse on 2>/dev/null; tmux set -g set-clipboard on 2>/dev/null; tmux attach -t ${sessionName} 2>/dev/null || ${tmuxNew}`
|
|
210
|
+
);
|
|
211
|
+
attachSession(paneId, session.id, sessionName);
|
|
212
|
+
terminal.focus();
|
|
213
|
+
},
|
|
214
|
+
[attachSession]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Attach session to terminal
|
|
218
|
+
const attachToSession = useCallback(
|
|
219
|
+
async (session: Session) => {
|
|
220
|
+
const terminalInfo = getTerminalWithFallback();
|
|
221
|
+
if (!terminalInfo) {
|
|
222
|
+
debugLog(
|
|
223
|
+
`ERROR: No terminal available to attach session: ${session.name}`
|
|
224
|
+
);
|
|
225
|
+
alert(
|
|
226
|
+
`[ClaudeDeck Debug] No terminal available!\n\nRun agentOSLogs() in console to see debug logs.`
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const { terminal, paneId } = terminalInfo;
|
|
232
|
+
const activeTab = getActiveTab(paneId);
|
|
233
|
+
const isInTmux = !!activeTab?.attachedTmux;
|
|
234
|
+
|
|
235
|
+
if (isInTmux) {
|
|
236
|
+
terminal.sendInput("\x02d");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setTimeout(
|
|
240
|
+
() => {
|
|
241
|
+
terminal.sendInput("\x03");
|
|
242
|
+
setTimeout(async () => {
|
|
243
|
+
const sessionInfo = await buildSessionCommand(session);
|
|
244
|
+
runSessionInTerminal(terminal, paneId, session, sessionInfo);
|
|
245
|
+
}, 50);
|
|
246
|
+
},
|
|
247
|
+
isInTmux ? 100 : 0
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
[
|
|
251
|
+
getTerminalWithFallback,
|
|
252
|
+
getActiveTab,
|
|
253
|
+
buildSessionCommand,
|
|
254
|
+
runSessionInTerminal,
|
|
255
|
+
]
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Open session in new tab
|
|
259
|
+
const openSessionInNewTab = useCallback(
|
|
260
|
+
(session: Session) => {
|
|
261
|
+
const existingKeys = new Set(terminalRefs.current.keys());
|
|
262
|
+
addTab(focusedPaneId);
|
|
263
|
+
|
|
264
|
+
let attempts = 0;
|
|
265
|
+
const maxAttempts = 20;
|
|
266
|
+
|
|
267
|
+
const waitForNewTerminal = () => {
|
|
268
|
+
attempts++;
|
|
269
|
+
|
|
270
|
+
for (const key of terminalRefs.current.keys()) {
|
|
271
|
+
if (!existingKeys.has(key) && key.startsWith(`${focusedPaneId}:`)) {
|
|
272
|
+
const terminal = terminalRefs.current.get(key);
|
|
273
|
+
if (terminal) {
|
|
274
|
+
buildSessionCommand(session).then((sessionInfo) => {
|
|
275
|
+
runSessionInTerminal(
|
|
276
|
+
terminal,
|
|
277
|
+
focusedPaneId,
|
|
278
|
+
session,
|
|
279
|
+
sessionInfo
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (attempts < maxAttempts) {
|
|
288
|
+
setTimeout(waitForNewTerminal, 50);
|
|
289
|
+
} else {
|
|
290
|
+
debugLog(`Failed to find new terminal after ${maxAttempts} attempts`);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
setTimeout(waitForNewTerminal, 50);
|
|
295
|
+
},
|
|
296
|
+
[addTab, focusedPaneId, buildSessionCommand, runSessionInTerminal]
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const resumeClaudeSession = useCallback(
|
|
300
|
+
(
|
|
301
|
+
claudeSessionId: string,
|
|
302
|
+
cwd: string,
|
|
303
|
+
sessionName?: string,
|
|
304
|
+
projectName?: string
|
|
305
|
+
) => {
|
|
306
|
+
const terminalInfo = getTerminalWithFallback();
|
|
307
|
+
if (!terminalInfo) return;
|
|
308
|
+
|
|
309
|
+
const { terminal, paneId } = terminalInfo;
|
|
310
|
+
const activeTab = getActiveTab(paneId);
|
|
311
|
+
const isInTmux = !!activeTab?.attachedTmux;
|
|
312
|
+
|
|
313
|
+
const tmuxName = `claude-${claudeSessionId}`;
|
|
314
|
+
const tmuxCmd = [
|
|
315
|
+
`tmux kill-session -t ${tmuxName} 2>/dev/null;`,
|
|
316
|
+
`tmux new -s ${tmuxName} -c "${cwd}" "claude --resume ${claudeSessionId} || claude --continue"`,
|
|
317
|
+
].join(" ");
|
|
318
|
+
|
|
319
|
+
if (isInTmux) {
|
|
320
|
+
terminal.sendInput("\x02d");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
setTimeout(
|
|
324
|
+
() => {
|
|
325
|
+
terminal.sendInput("\x03");
|
|
326
|
+
setTimeout(() => {
|
|
327
|
+
terminal.sendCommand(tmuxCmd);
|
|
328
|
+
attachSession(
|
|
329
|
+
paneId,
|
|
330
|
+
claudeSessionId,
|
|
331
|
+
tmuxName,
|
|
332
|
+
sessionName,
|
|
333
|
+
projectName,
|
|
334
|
+
cwd
|
|
335
|
+
);
|
|
336
|
+
terminal.focus();
|
|
337
|
+
}, 50);
|
|
338
|
+
},
|
|
339
|
+
isInTmux ? 100 : 0
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
[getTerminalWithFallback, getActiveTab, attachSession]
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const [newSessionPending, setNewSessionPending] = useState<{
|
|
346
|
+
cwd: string;
|
|
347
|
+
projectName: string;
|
|
348
|
+
} | null>(null);
|
|
349
|
+
|
|
350
|
+
const newClaudeSession = useCallback((cwd?: string, projectName?: string) => {
|
|
351
|
+
setNewSessionPending({ cwd: cwd || "~", projectName: projectName || "" });
|
|
352
|
+
}, []);
|
|
353
|
+
|
|
354
|
+
const handleNewClaudeSessionConfirm = useCallback(
|
|
355
|
+
(name: string, overrideCwd?: string, overrideProject?: string) => {
|
|
356
|
+
if (!newSessionPending) return;
|
|
357
|
+
setNewSessionPending(null);
|
|
358
|
+
|
|
359
|
+
const terminalInfo = getTerminalWithFallback();
|
|
360
|
+
if (!terminalInfo) return;
|
|
361
|
+
|
|
362
|
+
const { terminal, paneId } = terminalInfo;
|
|
363
|
+
const activeTab = getActiveTab(paneId);
|
|
364
|
+
const isInTmux = !!activeTab?.attachedTmux;
|
|
365
|
+
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
366
|
+
const tmuxName = `claude-new-${id}`;
|
|
367
|
+
const cwd = overrideCwd || newSessionPending.cwd;
|
|
368
|
+
const projectName = overrideProject || newSessionPending.projectName;
|
|
369
|
+
const tmuxCmd = `tmux new -s ${tmuxName} -c "${cwd}" "claude"`;
|
|
370
|
+
|
|
371
|
+
if (isInTmux) {
|
|
372
|
+
terminal.sendInput("\x02d");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
setTimeout(
|
|
376
|
+
() => {
|
|
377
|
+
terminal.sendInput("\x03");
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
terminal.sendCommand(tmuxCmd);
|
|
380
|
+
attachSession(paneId, id, tmuxName, name, projectName, cwd);
|
|
381
|
+
terminal.focus();
|
|
382
|
+
}, 50);
|
|
383
|
+
},
|
|
384
|
+
isInTmux ? 100 : 0
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (isMobile) setSidebarOpen(false);
|
|
388
|
+
},
|
|
389
|
+
[
|
|
390
|
+
newSessionPending,
|
|
391
|
+
getTerminalWithFallback,
|
|
392
|
+
getActiveTab,
|
|
393
|
+
attachSession,
|
|
394
|
+
isMobile,
|
|
395
|
+
]
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// Notification click handler
|
|
399
|
+
const handleNotificationClick = useCallback(
|
|
400
|
+
(sessionId: string) => {
|
|
401
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
402
|
+
if (session) {
|
|
403
|
+
attachToSession(session);
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
[sessions, attachToSession]
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Notifications
|
|
410
|
+
const {
|
|
411
|
+
settings: notificationSettings,
|
|
412
|
+
checkStateChanges,
|
|
413
|
+
updateSettings,
|
|
414
|
+
requestPermission,
|
|
415
|
+
permissionGranted,
|
|
416
|
+
} = useNotifications({ onSessionClick: handleNotificationClick });
|
|
417
|
+
|
|
418
|
+
// Session statuses
|
|
419
|
+
const { sessionStatuses } = useSessionStatuses({
|
|
420
|
+
sessions,
|
|
421
|
+
activeSessionId: focusedActiveTab?.sessionId,
|
|
422
|
+
checkStateChanges,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Set initial sidebar state based on viewport (only after hydration)
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (isHydrated && !isMobile) setSidebarOpen(true);
|
|
428
|
+
}, [isMobile, isHydrated]);
|
|
429
|
+
|
|
430
|
+
// Keyboard shortcut: Cmd+K to open quick switcher
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
433
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
setShowQuickSwitcher(true);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
439
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
440
|
+
}, []);
|
|
441
|
+
|
|
442
|
+
// Session selection handler
|
|
443
|
+
const handleSelectSession = useCallback(
|
|
444
|
+
(sessionId: string) => {
|
|
445
|
+
debugLog(`handleSelectSession called for: ${sessionId}`);
|
|
446
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
447
|
+
if (session) {
|
|
448
|
+
debugLog(`Found session: ${session.name}, calling attachToSession`);
|
|
449
|
+
attachToSession(session);
|
|
450
|
+
} else {
|
|
451
|
+
debugLog(
|
|
452
|
+
`Session not found in sessions array (length: ${sessions.length})`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[sessions, attachToSession]
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Pane renderer
|
|
460
|
+
const renderPane = useCallback(
|
|
461
|
+
(paneId: string) => (
|
|
462
|
+
<Pane
|
|
463
|
+
key={paneId}
|
|
464
|
+
paneId={paneId}
|
|
465
|
+
sessions={sessions}
|
|
466
|
+
projects={projects}
|
|
467
|
+
onRegisterTerminal={registerTerminalRef}
|
|
468
|
+
onMenuClick={isMobile ? () => setSidebarOpen(true) : undefined}
|
|
469
|
+
onSelectSession={handleSelectSession}
|
|
470
|
+
onResumeClaudeSession={resumeClaudeSession}
|
|
471
|
+
/>
|
|
472
|
+
),
|
|
473
|
+
[
|
|
474
|
+
sessions,
|
|
475
|
+
projects,
|
|
476
|
+
registerTerminalRef,
|
|
477
|
+
isMobile,
|
|
478
|
+
handleSelectSession,
|
|
479
|
+
resumeClaudeSession,
|
|
480
|
+
]
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// New session in project handler
|
|
484
|
+
const handleNewSessionInProject = useCallback((projectId: string) => {
|
|
485
|
+
setNewSessionProjectId(projectId);
|
|
486
|
+
setShowNewSessionDialog(true);
|
|
487
|
+
}, []);
|
|
488
|
+
|
|
489
|
+
// Session created handler (shared between desktop/mobile)
|
|
490
|
+
const handleSessionCreated = useCallback(
|
|
491
|
+
async (sessionId: string) => {
|
|
492
|
+
setShowNewSessionDialog(false);
|
|
493
|
+
setNewSessionProjectId(null);
|
|
494
|
+
await fetchSessions();
|
|
495
|
+
|
|
496
|
+
const res = await fetch(`/api/sessions/${sessionId}`);
|
|
497
|
+
const data = await res.json();
|
|
498
|
+
if (!data.session) return;
|
|
499
|
+
|
|
500
|
+
setTimeout(() => attachToSession(data.session), 100);
|
|
501
|
+
},
|
|
502
|
+
[fetchSessions, attachToSession]
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// Project created handler (shared between desktop/mobile)
|
|
506
|
+
const handleCreateProject = useCallback(
|
|
507
|
+
async (
|
|
508
|
+
name: string,
|
|
509
|
+
workingDirectory: string,
|
|
510
|
+
agentType?: string
|
|
511
|
+
): Promise<string | null> => {
|
|
512
|
+
const res = await fetch("/api/projects", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers: { "Content-Type": "application/json" },
|
|
515
|
+
body: JSON.stringify({ name, workingDirectory, agentType }),
|
|
516
|
+
});
|
|
517
|
+
const data = await res.json();
|
|
518
|
+
if (data.project) {
|
|
519
|
+
await fetchProjects();
|
|
520
|
+
return data.project.id;
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
},
|
|
524
|
+
[fetchProjects]
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Open terminal in project handler (shell session, not AI agent)
|
|
528
|
+
const handleOpenTerminal = useCallback(
|
|
529
|
+
async (projectId: string) => {
|
|
530
|
+
const project = projects.find((p) => p.id === projectId);
|
|
531
|
+
if (!project) return;
|
|
532
|
+
|
|
533
|
+
// Create a shell session with the project's working directory
|
|
534
|
+
const res = await fetch("/api/sessions", {
|
|
535
|
+
method: "POST",
|
|
536
|
+
headers: { "Content-Type": "application/json" },
|
|
537
|
+
body: JSON.stringify({
|
|
538
|
+
name: `${project.name} Terminal`,
|
|
539
|
+
workingDirectory: project.working_directory || "~",
|
|
540
|
+
agentType: "shell",
|
|
541
|
+
projectId,
|
|
542
|
+
}),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const data = await res.json();
|
|
546
|
+
if (!data.session) return;
|
|
547
|
+
|
|
548
|
+
await fetchSessions();
|
|
549
|
+
|
|
550
|
+
// Small delay to ensure state updates, then attach
|
|
551
|
+
setTimeout(() => {
|
|
552
|
+
attachToSession(data.session);
|
|
553
|
+
}, 100);
|
|
554
|
+
},
|
|
555
|
+
[projects, fetchSessions, attachToSession]
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Active session and dev server project
|
|
559
|
+
const activeSession = sessions.find(
|
|
560
|
+
(s) => s.id === focusedActiveTab?.sessionId
|
|
561
|
+
);
|
|
562
|
+
const startDevServerProject = startDevServerProjectId
|
|
563
|
+
? (projects.find((p) => p.id === startDevServerProjectId) ?? null)
|
|
564
|
+
: null;
|
|
565
|
+
|
|
566
|
+
// View props
|
|
567
|
+
const viewProps = {
|
|
568
|
+
sessions,
|
|
569
|
+
projects,
|
|
570
|
+
sessionStatuses,
|
|
571
|
+
sidebarOpen,
|
|
572
|
+
setSidebarOpen,
|
|
573
|
+
activeSession,
|
|
574
|
+
focusedActiveTab,
|
|
575
|
+
copiedSessionId,
|
|
576
|
+
setCopiedSessionId,
|
|
577
|
+
showNewSessionDialog,
|
|
578
|
+
setShowNewSessionDialog,
|
|
579
|
+
newSessionProjectId,
|
|
580
|
+
showNotificationSettings,
|
|
581
|
+
setShowNotificationSettings,
|
|
582
|
+
showQuickSwitcher,
|
|
583
|
+
setShowQuickSwitcher,
|
|
584
|
+
notificationSettings,
|
|
585
|
+
permissionGranted,
|
|
586
|
+
updateSettings,
|
|
587
|
+
requestPermission,
|
|
588
|
+
attachToSession,
|
|
589
|
+
openSessionInNewTab,
|
|
590
|
+
handleNewSessionInProject,
|
|
591
|
+
handleOpenTerminal,
|
|
592
|
+
handleSessionCreated,
|
|
593
|
+
handleCreateProject,
|
|
594
|
+
handleStartDevServer: startDevServer,
|
|
595
|
+
handleCreateDevServer: createDevServer,
|
|
596
|
+
startDevServerProject,
|
|
597
|
+
setStartDevServerProjectId,
|
|
598
|
+
newClaudeSession,
|
|
599
|
+
resumeClaudeSession,
|
|
600
|
+
renderPane,
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const view = isMobile ? (
|
|
604
|
+
<MobileView {...viewProps} />
|
|
605
|
+
) : (
|
|
606
|
+
<DesktopView {...viewProps} />
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
<>
|
|
611
|
+
{view}
|
|
612
|
+
<NewClaudeSessionDialog
|
|
613
|
+
open={!!newSessionPending}
|
|
614
|
+
projectName={newSessionPending?.projectName || ""}
|
|
615
|
+
projects={claudeProjects}
|
|
616
|
+
onClose={() => setNewSessionPending(null)}
|
|
617
|
+
onConfirm={handleNewClaudeSessionConfirm}
|
|
618
|
+
/>
|
|
619
|
+
</>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export default function Home() {
|
|
624
|
+
return (
|
|
625
|
+
<PaneProvider>
|
|
626
|
+
<HomeContent />
|
|
627
|
+
</PaneProvider>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { User, Bot } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface ChatMessageProps {
|
|
7
|
+
role: "user" | "assistant";
|
|
8
|
+
content: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
isStreaming?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatMessage({
|
|
14
|
+
role,
|
|
15
|
+
content,
|
|
16
|
+
timestamp,
|
|
17
|
+
isStreaming,
|
|
18
|
+
}: ChatMessageProps) {
|
|
19
|
+
const isUser = role === "user";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
"flex gap-3 rounded-lg p-4",
|
|
25
|
+
isUser ? "bg-muted/50" : "bg-card"
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full",
|
|
31
|
+
isUser ? "bg-primary" : "bg-muted"
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{isUser ? (
|
|
35
|
+
<User className="text-primary-foreground h-4 w-4" />
|
|
36
|
+
) : (
|
|
37
|
+
<Bot className="text-muted-foreground h-4 w-4" />
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div className="min-w-0 flex-1">
|
|
42
|
+
<div className="mb-1 flex items-center gap-2">
|
|
43
|
+
<span className="text-sm font-medium">
|
|
44
|
+
{isUser ? "You" : "Claude"}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="text-muted-foreground text-xs">
|
|
47
|
+
{new Date(timestamp).toLocaleTimeString()}
|
|
48
|
+
</span>
|
|
49
|
+
{isStreaming && (
|
|
50
|
+
<span className="text-primary animate-pulse text-xs">
|
|
51
|
+
streaming...
|
|
52
|
+
</span>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="text-sm break-words whitespace-pre-wrap">
|
|
57
|
+
{content}
|
|
58
|
+
{isStreaming && (
|
|
59
|
+
<span className="bg-primary ml-0.5 inline-block h-4 w-2 animate-pulse" />
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|