@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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal constants and theme configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getTerminalTheme, type TerminalTheme } from "@/lib/terminal-themes";
|
|
6
|
+
|
|
7
|
+
// Reconnection constants
|
|
8
|
+
export const WS_RECONNECT_BASE_DELAY = 1000; // 1 second
|
|
9
|
+
export const WS_RECONNECT_MAX_DELAY = 30000; // 30 seconds
|
|
10
|
+
export const WS_INACTIVITY_TIMEOUT = 45000; // 45 seconds - force reconnect if no data
|
|
11
|
+
|
|
12
|
+
// Get terminal theme for current app theme
|
|
13
|
+
export function getTerminalThemeForApp(theme: string): TerminalTheme {
|
|
14
|
+
return getTerminalTheme(theme);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Legacy exports for compatibility - default dark themes
|
|
18
|
+
export const TERMINAL_THEME_DARK = getTerminalTheme("dark");
|
|
19
|
+
export const TERMINAL_THEME_LIGHT = getTerminalTheme("light");
|
|
20
|
+
export const TERMINAL_THEME = TERMINAL_THEME_DARK;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Terminal as XTerm } from "@xterm/xterm";
|
|
4
|
+
import type { FitAddon } from "@xterm/addon-fit";
|
|
5
|
+
|
|
6
|
+
interface ResizeHandlersConfig {
|
|
7
|
+
term: XTerm;
|
|
8
|
+
fitAddon: FitAddon;
|
|
9
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
10
|
+
isMobile: boolean;
|
|
11
|
+
sendResize: (cols: number, rows: number) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function setupResizeHandlers(config: ResizeHandlersConfig): () => void {
|
|
15
|
+
const { term, fitAddon, containerRef, isMobile, sendResize } = config;
|
|
16
|
+
|
|
17
|
+
let resizeTimeout: NodeJS.Timeout | null = null;
|
|
18
|
+
let fitTimeouts: NodeJS.Timeout[] = [];
|
|
19
|
+
const mqListeners: { mq: MediaQueryList; handler: () => void }[] = [];
|
|
20
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
21
|
+
|
|
22
|
+
// Workaround for FitAddon bug: it reserves 14px for scrollbar even when hidden
|
|
23
|
+
// FitAddon uses `overviewRuler?.width || 14` which doesn't work with 0 (falsy)
|
|
24
|
+
// On mobile we hide the scrollbar via CSS, so manually expand xterm-screen to full width
|
|
25
|
+
const fixMobileScrollbarWidth = () => {
|
|
26
|
+
if (!isMobile || !containerRef.current) return;
|
|
27
|
+
|
|
28
|
+
const xtermScreen = containerRef.current.querySelector(
|
|
29
|
+
".xterm-screen"
|
|
30
|
+
) as HTMLElement | null;
|
|
31
|
+
if (xtermScreen) {
|
|
32
|
+
const containerWidth = containerRef.current.clientWidth;
|
|
33
|
+
xtermScreen.style.width = `${containerWidth}px`;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const doFit = () => {
|
|
38
|
+
// Clear any pending fit timeouts
|
|
39
|
+
fitTimeouts.forEach(clearTimeout);
|
|
40
|
+
fitTimeouts = [];
|
|
41
|
+
|
|
42
|
+
// On mobile, save scroll position before fit to prevent keyboard open/close scroll
|
|
43
|
+
const savedScrollLine = isMobile ? term.buffer.active.viewportY : null;
|
|
44
|
+
|
|
45
|
+
const restoreScroll = () => {
|
|
46
|
+
if (savedScrollLine !== null) {
|
|
47
|
+
term.scrollToLine(savedScrollLine);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
// First fit - immediate
|
|
53
|
+
fitAddon.fit();
|
|
54
|
+
fixMobileScrollbarWidth();
|
|
55
|
+
restoreScroll();
|
|
56
|
+
sendResize(term.cols, term.rows);
|
|
57
|
+
|
|
58
|
+
// Second fit - after 100ms (handles most delayed layout updates)
|
|
59
|
+
fitTimeouts.push(
|
|
60
|
+
setTimeout(() => {
|
|
61
|
+
fitAddon.fit();
|
|
62
|
+
fixMobileScrollbarWidth();
|
|
63
|
+
restoreScroll();
|
|
64
|
+
sendResize(term.cols, term.rows);
|
|
65
|
+
}, 100)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Third fit - after 250ms (handles slow layout updates, e.g., DevTools toggle)
|
|
69
|
+
fitTimeouts.push(
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
fitAddon.fit();
|
|
72
|
+
fixMobileScrollbarWidth();
|
|
73
|
+
restoreScroll();
|
|
74
|
+
sendResize(term.cols, term.rows);
|
|
75
|
+
}, 250)
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleResize = () => {
|
|
81
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
82
|
+
resizeTimeout = setTimeout(doFit, isMobile ? 100 : 50);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Window resize
|
|
86
|
+
window.addEventListener("resize", handleResize);
|
|
87
|
+
|
|
88
|
+
// Media query listeners for Chrome DevTools mobile toggle
|
|
89
|
+
const mediaQueries = [
|
|
90
|
+
"(max-width: 640px)",
|
|
91
|
+
"(max-width: 768px)",
|
|
92
|
+
"(max-width: 1024px)",
|
|
93
|
+
];
|
|
94
|
+
mediaQueries.forEach((query) => {
|
|
95
|
+
const mq = window.matchMedia(query);
|
|
96
|
+
const handler = () => handleResize();
|
|
97
|
+
mq.addEventListener("change", handler);
|
|
98
|
+
mqListeners.push({ mq, handler });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Handle orientation change on mobile
|
|
102
|
+
if (isMobile && "orientation" in screen) {
|
|
103
|
+
screen.orientation.addEventListener("change", handleResize);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle visual viewport changes (for mobile keyboard)
|
|
107
|
+
if (isMobile && window.visualViewport) {
|
|
108
|
+
window.visualViewport.addEventListener("resize", handleResize);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ResizeObserver for container changes
|
|
112
|
+
if (containerRef.current) {
|
|
113
|
+
resizeObserver = new ResizeObserver(() => handleResize());
|
|
114
|
+
resizeObserver.observe(containerRef.current);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Return cleanup function
|
|
118
|
+
return () => {
|
|
119
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
120
|
+
fitTimeouts.forEach(clearTimeout);
|
|
121
|
+
|
|
122
|
+
window.removeEventListener("resize", handleResize);
|
|
123
|
+
|
|
124
|
+
mqListeners.forEach(({ mq, handler }) => {
|
|
125
|
+
mq.removeEventListener("change", handler);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (isMobile && "orientation" in screen) {
|
|
129
|
+
screen.orientation.removeEventListener("change", handleResize);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isMobile && window.visualViewport) {
|
|
133
|
+
window.visualViewport.removeEventListener("resize", handleResize);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (resizeObserver) {
|
|
137
|
+
resizeObserver.disconnect();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Terminal as XTerm } from "@xterm/xterm";
|
|
4
|
+
import { FitAddon } from "@xterm/addon-fit";
|
|
5
|
+
import { WebLinksAddon } from "@xterm/addon-web-links";
|
|
6
|
+
import { SearchAddon } from "@xterm/addon-search";
|
|
7
|
+
import { CanvasAddon } from "@xterm/addon-canvas";
|
|
8
|
+
import { getTerminalThemeForApp } from "../constants";
|
|
9
|
+
|
|
10
|
+
export interface TerminalInstance {
|
|
11
|
+
term: XTerm;
|
|
12
|
+
fitAddon: FitAddon;
|
|
13
|
+
searchAddon: SearchAddon;
|
|
14
|
+
cleanup: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createTerminal(
|
|
18
|
+
container: HTMLElement,
|
|
19
|
+
isMobile: boolean,
|
|
20
|
+
theme: string
|
|
21
|
+
): TerminalInstance {
|
|
22
|
+
const fontSize = isMobile ? 11 : 14;
|
|
23
|
+
const terminalTheme = getTerminalThemeForApp(theme || "dark");
|
|
24
|
+
|
|
25
|
+
const term = new XTerm({
|
|
26
|
+
cursorBlink: true,
|
|
27
|
+
fontSize,
|
|
28
|
+
fontFamily:
|
|
29
|
+
'"JetBrains Mono", "Fira Code", Menlo, Monaco, "Courier New", monospace',
|
|
30
|
+
fontWeight: "400",
|
|
31
|
+
fontWeightBold: "600",
|
|
32
|
+
letterSpacing: 0,
|
|
33
|
+
lineHeight: isMobile ? 1.15 : 1.2,
|
|
34
|
+
scrollback: 15000,
|
|
35
|
+
scrollSensitivity: isMobile ? 3 : 1,
|
|
36
|
+
fastScrollSensitivity: 5,
|
|
37
|
+
smoothScrollDuration: 100,
|
|
38
|
+
cursorStyle: "bar",
|
|
39
|
+
cursorWidth: 2,
|
|
40
|
+
allowProposedApi: true,
|
|
41
|
+
theme: terminalTheme,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const fitAddon = new FitAddon();
|
|
45
|
+
const searchAddon = new SearchAddon();
|
|
46
|
+
|
|
47
|
+
term.loadAddon(fitAddon);
|
|
48
|
+
term.loadAddon(new WebLinksAddon());
|
|
49
|
+
term.loadAddon(searchAddon);
|
|
50
|
+
term.open(container);
|
|
51
|
+
term.loadAddon(new CanvasAddon());
|
|
52
|
+
fitAddon.fit();
|
|
53
|
+
|
|
54
|
+
// Helper to copy text to clipboard with fallback
|
|
55
|
+
const copyToClipboard = (text: string) => {
|
|
56
|
+
if (navigator.clipboard?.writeText) {
|
|
57
|
+
navigator.clipboard.writeText(text).catch(() => {
|
|
58
|
+
// Fallback if clipboard API fails
|
|
59
|
+
execCommandCopy(text);
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
// Fallback for non-secure contexts
|
|
63
|
+
execCommandCopy(text);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const execCommandCopy = (text: string) => {
|
|
68
|
+
const textarea = document.createElement("textarea");
|
|
69
|
+
textarea.value = text;
|
|
70
|
+
textarea.style.position = "fixed";
|
|
71
|
+
textarea.style.opacity = "0";
|
|
72
|
+
document.body.appendChild(textarea);
|
|
73
|
+
textarea.select();
|
|
74
|
+
document.execCommand("copy");
|
|
75
|
+
document.body.removeChild(textarea);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Handle Cmd+A and Cmd+C via document event listener (more reliable than attachCustomKeyEventHandler)
|
|
79
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
80
|
+
// Only handle when terminal is focused (xterm creates its textarea inside the container)
|
|
81
|
+
if (!container.contains(document.activeElement)) return;
|
|
82
|
+
|
|
83
|
+
const key = event.key.toLowerCase();
|
|
84
|
+
|
|
85
|
+
// Cmd+A (macOS) / Ctrl+A for select all
|
|
86
|
+
if ((event.metaKey || event.ctrlKey) && key === "a") {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
term.selectAll();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Cmd+C (macOS) / Ctrl+C for copy when text is selected
|
|
94
|
+
if ((event.metaKey || event.ctrlKey) && key === "c") {
|
|
95
|
+
const selection = term.getSelection();
|
|
96
|
+
if (selection) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.stopPropagation();
|
|
99
|
+
copyToClipboard(selection);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Use capture phase to intercept before browser default
|
|
105
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
106
|
+
|
|
107
|
+
// Handle OSC 52 (clipboard) sequences from tmux
|
|
108
|
+
const osc52Disposable = term.parser.registerOscHandler(52, (data) => {
|
|
109
|
+
const parts = data.split(";");
|
|
110
|
+
if (parts.length >= 2) {
|
|
111
|
+
const base64 = parts[parts.length - 1];
|
|
112
|
+
if (base64 && base64 !== "?") {
|
|
113
|
+
try {
|
|
114
|
+
const text = atob(base64);
|
|
115
|
+
copyToClipboard(text);
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const cleanup = () => {
|
|
123
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
124
|
+
osc52Disposable.dispose();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return { term, fitAddon, searchAddon, cleanup };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function updateTerminalForMobile(
|
|
131
|
+
term: XTerm,
|
|
132
|
+
fitAddon: FitAddon,
|
|
133
|
+
isMobile: boolean,
|
|
134
|
+
sendResize: (cols: number, rows: number) => void
|
|
135
|
+
): void {
|
|
136
|
+
const newFontSize = isMobile ? 11 : 14;
|
|
137
|
+
const newLineHeight = isMobile ? 1.15 : 1.2;
|
|
138
|
+
|
|
139
|
+
if (term.options.fontSize !== newFontSize) {
|
|
140
|
+
term.options.fontSize = newFontSize;
|
|
141
|
+
term.options.lineHeight = newLineHeight;
|
|
142
|
+
term.refresh(0, term.rows - 1);
|
|
143
|
+
fitAddon.fit();
|
|
144
|
+
sendResize(term.cols, term.rows);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function updateTerminalTheme(term: XTerm, theme: string): void {
|
|
149
|
+
const terminalTheme = getTerminalThemeForApp(theme || "dark");
|
|
150
|
+
term.options.theme = terminalTheme;
|
|
151
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Terminal as XTerm } from "@xterm/xterm";
|
|
4
|
+
import type { RefObject } from "react";
|
|
5
|
+
|
|
6
|
+
interface TouchScrollConfig {
|
|
7
|
+
term: XTerm;
|
|
8
|
+
selectModeRef: RefObject<boolean>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function setupTouchScroll(config: TouchScrollConfig): () => void {
|
|
12
|
+
const { term, selectModeRef } = config;
|
|
13
|
+
|
|
14
|
+
if (!term.element) return () => {};
|
|
15
|
+
|
|
16
|
+
let touchElement: HTMLElement | null = null;
|
|
17
|
+
let handleTouchStart: ((e: TouchEvent) => void) | null = null;
|
|
18
|
+
let handleTouchMove: ((e: TouchEvent) => void) | null = null;
|
|
19
|
+
let handleTouchEnd: (() => void) | null = null;
|
|
20
|
+
let handleTouchCancel: (() => void) | null = null;
|
|
21
|
+
let setupTimeout: NodeJS.Timeout | null = null;
|
|
22
|
+
|
|
23
|
+
const setupTouchScrollInner = () => {
|
|
24
|
+
const xtermScreen = term.element?.querySelector(
|
|
25
|
+
".xterm-screen"
|
|
26
|
+
) as HTMLElement | null;
|
|
27
|
+
if (!xtermScreen) {
|
|
28
|
+
setupTimeout = setTimeout(setupTouchScrollInner, 50);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
xtermScreen.style.touchAction = "none";
|
|
33
|
+
xtermScreen.style.userSelect = "none";
|
|
34
|
+
(
|
|
35
|
+
xtermScreen.style as CSSStyleDeclaration & { webkitUserSelect?: string }
|
|
36
|
+
).webkitUserSelect = "none";
|
|
37
|
+
|
|
38
|
+
const canvases = xtermScreen.querySelectorAll("canvas");
|
|
39
|
+
canvases.forEach((canvas) => {
|
|
40
|
+
(canvas as HTMLElement).style.touchAction = "none";
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Prevent native scroll on the xterm-viewport and scrollable-element wrappers
|
|
44
|
+
// which can intercept touch events on mobile before they reach .xterm-screen
|
|
45
|
+
const viewport = term.element?.querySelector(
|
|
46
|
+
".xterm-viewport"
|
|
47
|
+
) as HTMLElement | null;
|
|
48
|
+
if (viewport) {
|
|
49
|
+
viewport.style.touchAction = "none";
|
|
50
|
+
viewport.style.overflowY = "hidden";
|
|
51
|
+
}
|
|
52
|
+
const scrollableEl = term.element?.querySelector(
|
|
53
|
+
".xterm-scrollable-element"
|
|
54
|
+
) as HTMLElement | null;
|
|
55
|
+
if (scrollableEl) {
|
|
56
|
+
scrollableEl.style.touchAction = "none";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let startX = 0;
|
|
60
|
+
let startY = 0;
|
|
61
|
+
let lastY = 0;
|
|
62
|
+
let scrollDirection: "vertical" | "horizontal" | null = null;
|
|
63
|
+
let scrollAccumulator = 0;
|
|
64
|
+
|
|
65
|
+
handleTouchStart = (e: TouchEvent) => {
|
|
66
|
+
if (selectModeRef.current || e.touches.length === 0) return;
|
|
67
|
+
const touch = e.touches[0];
|
|
68
|
+
startX = touch.clientX;
|
|
69
|
+
startY = touch.clientY;
|
|
70
|
+
lastY = touch.clientY;
|
|
71
|
+
scrollDirection = null;
|
|
72
|
+
scrollAccumulator = 0;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
handleTouchMove = (e: TouchEvent) => {
|
|
76
|
+
if (selectModeRef.current || e.touches.length === 0) return;
|
|
77
|
+
if (scrollDirection === null && startX === 0 && startY === 0) return;
|
|
78
|
+
|
|
79
|
+
const touch = e.touches[0];
|
|
80
|
+
|
|
81
|
+
if (scrollDirection === null) {
|
|
82
|
+
const deltaX = Math.abs(touch.clientX - startX);
|
|
83
|
+
const deltaY = Math.abs(touch.clientY - startY);
|
|
84
|
+
if (deltaX > 8 || deltaY > 8) {
|
|
85
|
+
scrollDirection = deltaX > deltaY ? "horizontal" : "vertical";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (scrollDirection === "horizontal") return;
|
|
90
|
+
|
|
91
|
+
if (scrollDirection === "vertical") {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const deltaY = touch.clientY - lastY;
|
|
97
|
+
lastY = touch.clientY;
|
|
98
|
+
|
|
99
|
+
if (Math.abs(deltaY) < 1) return;
|
|
100
|
+
|
|
101
|
+
scrollAccumulator += deltaY;
|
|
102
|
+
const step = 20;
|
|
103
|
+
|
|
104
|
+
while (Math.abs(scrollAccumulator) >= step) {
|
|
105
|
+
const wheelDelta = scrollAccumulator > 0 ? -100 : 100;
|
|
106
|
+
const syntheticWheel = new WheelEvent("wheel", {
|
|
107
|
+
deltaY: wheelDelta,
|
|
108
|
+
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
|
109
|
+
bubbles: true,
|
|
110
|
+
cancelable: true,
|
|
111
|
+
clientX: touch.clientX,
|
|
112
|
+
clientY: touch.clientY,
|
|
113
|
+
});
|
|
114
|
+
xtermScreen.dispatchEvent(syntheticWheel);
|
|
115
|
+
scrollAccumulator += scrollAccumulator > 0 ? -step : step;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
handleTouchEnd = () => {
|
|
120
|
+
startX = 0;
|
|
121
|
+
startY = 0;
|
|
122
|
+
lastY = 0;
|
|
123
|
+
scrollDirection = null;
|
|
124
|
+
scrollAccumulator = 0;
|
|
125
|
+
};
|
|
126
|
+
handleTouchCancel = handleTouchEnd;
|
|
127
|
+
|
|
128
|
+
xtermScreen.addEventListener("touchstart", handleTouchStart, {
|
|
129
|
+
passive: true,
|
|
130
|
+
});
|
|
131
|
+
xtermScreen.addEventListener("touchmove", handleTouchMove, {
|
|
132
|
+
passive: false,
|
|
133
|
+
});
|
|
134
|
+
xtermScreen.addEventListener("touchend", handleTouchEnd);
|
|
135
|
+
xtermScreen.addEventListener("touchcancel", handleTouchCancel);
|
|
136
|
+
|
|
137
|
+
touchElement = xtermScreen;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
setupTouchScrollInner();
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
if (setupTimeout) clearTimeout(setupTimeout);
|
|
144
|
+
if (touchElement) {
|
|
145
|
+
if (handleTouchStart)
|
|
146
|
+
touchElement.removeEventListener("touchstart", handleTouchStart);
|
|
147
|
+
if (handleTouchMove)
|
|
148
|
+
touchElement.removeEventListener("touchmove", handleTouchMove);
|
|
149
|
+
if (handleTouchEnd)
|
|
150
|
+
touchElement.removeEventListener("touchend", handleTouchEnd);
|
|
151
|
+
if (handleTouchCancel)
|
|
152
|
+
touchElement.removeEventListener("touchcancel", handleTouchCancel);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|