Skip to content

Commit b4a544a

Browse files
authored
Merge pull request #691 from raghavyuva/feat/border-of-card
perf: improve terminal ui / ux performance
2 parents 22f72dc + 2797286 commit b4a544a

File tree

14 files changed

+601
-227
lines changed

14 files changed

+601
-227
lines changed

api/api/versions.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"version": "v1",
55
"status": "active",
6-
"release_date": "2025-12-10T19:26:08.572107+05:30",
6+
"release_date": "2025-12-10T22:01:46.6664+05:30",
77
"end_of_life": "0001-01-01T00:00:00Z",
88
"changes": [
99
"Initial API version"

view/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
66
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap');
77
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap');
8+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
9+
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&display=swap');
810
@import 'tailwindcss';
911

1012
@plugin "tailwindcss-animate";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { X } from 'lucide-react';
5+
import { cn } from '@/lib/utils';
6+
import type { SessionStatus } from './TerminalSession';
7+
8+
type SessionTabProps = {
9+
session: { id: string; label: string };
10+
isActive: boolean;
11+
status: SessionStatus;
12+
onSelect: () => void;
13+
onClose: () => void;
14+
canClose: boolean;
15+
index: number;
16+
};
17+
18+
export const SessionTab: React.FC<SessionTabProps> = ({
19+
session,
20+
isActive,
21+
status,
22+
onSelect,
23+
onClose,
24+
canClose,
25+
index
26+
}) => {
27+
return (
28+
<div
29+
className={cn(
30+
'group relative flex items-center gap-1.5 px-2 py-1 rounded-md cursor-pointer transition-all duration-200 shrink-0',
31+
'hover:bg-[var(--terminal-tab-hover)]',
32+
isActive && 'bg-[var(--terminal-tab-active)] terminal-tab-active'
33+
)}
34+
onClick={onSelect}
35+
style={{
36+
animation: `terminalFadeIn 0.2s ease-out ${index * 0.05}s both`
37+
}}
38+
>
39+
<div className="relative flex items-center justify-center w-3 h-3">
40+
{status === 'loading' ? (
41+
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
42+
) : status === 'active' ? (
43+
<div
44+
className={cn(
45+
'w-2 h-2 rounded-full bg-emerald-400',
46+
isActive && 'terminal-ready-indicator'
47+
)}
48+
/>
49+
) : (
50+
<div className="w-2 h-2 rounded-full bg-zinc-500" />
51+
)}
52+
</div>
53+
54+
<span
55+
className={cn(
56+
'text-xs font-medium transition-colors duration-200 whitespace-nowrap',
57+
isActive ? 'text-[var(--terminal-text)]' : 'text-[var(--terminal-text-muted)]'
58+
)}
59+
>
60+
{session.label}
61+
</span>
62+
63+
{canClose && (
64+
<button
65+
className={cn(
66+
'ml-1 p-0.5 rounded transition-all duration-200',
67+
'opacity-0 group-hover:opacity-100',
68+
'hover:bg-white/10 text-[var(--terminal-text-muted)] hover:text-[var(--terminal-text)]'
69+
)}
70+
onClick={(e) => {
71+
e.stopPropagation();
72+
onClose();
73+
}}
74+
>
75+
<X className="h-3 w-3" />
76+
</button>
77+
)}
78+
</div>
79+
);
80+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
'use client';
2+
3+
import React from 'react';
4+
import { Plus, X, Zap } from 'lucide-react';
5+
import { Button } from '@/components/ui/button';
6+
import { cn } from '@/lib/utils';
7+
import { SessionTab } from './SessionTab';
8+
import type { SessionStatus } from './TerminalSession';
9+
10+
type Session = {
11+
id: string;
12+
label: string;
13+
};
14+
15+
type TerminalHeaderProps = {
16+
sessions: Session[];
17+
activeSessionId: string;
18+
sessionStatuses: Record<string, SessionStatus>;
19+
sessionLimit: number;
20+
onAddSession: () => void;
21+
onCloseSession: (id: string) => void;
22+
onSwitchSession: (id: string) => void;
23+
onToggleTerminal: () => void;
24+
closeLabel: string;
25+
newTabLabel: string;
26+
};
27+
28+
export const TerminalHeader: React.FC<TerminalHeaderProps> = ({
29+
sessions,
30+
activeSessionId,
31+
sessionStatuses,
32+
sessionLimit,
33+
onAddSession,
34+
onCloseSession,
35+
onSwitchSession,
36+
onToggleTerminal,
37+
closeLabel,
38+
newTabLabel
39+
}) => {
40+
return (
41+
<div
42+
className="flex h-10 min-h-10 items-center px-2 gap-2 overflow-hidden shrink-0"
43+
style={{
44+
borderBottom: '1px solid var(--terminal-border)',
45+
background: 'rgba(18, 18, 22, 0.98)',
46+
width: '100%',
47+
maxWidth: '100%',
48+
boxSizing: 'border-box'
49+
}}
50+
>
51+
<div className="flex-1 min-w-0 overflow-hidden">
52+
<div className="flex items-center gap-1 overflow-x-auto no-scrollbar">
53+
{sessions.map((session, index) => (
54+
<SessionTab
55+
key={session.id}
56+
session={session}
57+
isActive={session.id === activeSessionId}
58+
status={sessionStatuses[session.id] || 'idle'}
59+
onSelect={() => onSwitchSession(session.id)}
60+
onClose={() => onCloseSession(session.id)}
61+
canClose={sessions.length > 1}
62+
index={index}
63+
/>
64+
))}
65+
66+
{sessions.length < sessionLimit && (
67+
<button
68+
className={cn(
69+
'flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 shrink-0',
70+
'hover:bg-[var(--terminal-tab-hover)] text-[var(--terminal-text-muted)]',
71+
'hover:text-[var(--terminal-accent)]'
72+
)}
73+
onClick={onAddSession}
74+
title={newTabLabel}
75+
>
76+
<Plus className="h-3.5 w-3.5" />
77+
</button>
78+
)}
79+
</div>
80+
</div>
81+
82+
<div className="flex items-center gap-1 shrink-0">
83+
<div className="hidden sm:flex items-center gap-1 px-1.5 py-0.5 rounded bg-white/5">
84+
<Zap className="h-2.5 w-2.5 text-amber-400" />
85+
<span className="text-[9px] font-medium text-[var(--terminal-text-muted)]">
86+
{sessions.length}/{sessionLimit}
87+
</span>
88+
</div>
89+
90+
<Button
91+
variant="ghost"
92+
size="icon"
93+
onClick={onToggleTerminal}
94+
title={closeLabel}
95+
className={cn(
96+
'h-6 w-6 rounded-md transition-all duration-200',
97+
'hover:bg-red-500/10 hover:text-red-400',
98+
'text-[var(--terminal-text-muted)]'
99+
)}
100+
>
101+
<X className="h-3.5 w-3.5" />
102+
</Button>
103+
</div>
104+
</div>
105+
);
106+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
'use client';
2+
3+
import React, { useEffect, useRef } from 'react';
4+
import { useTerminal } from '../utils/useTerminal';
5+
import { useContainerReady } from '../utils/isContainerReady';
6+
import { cn } from '@/lib/utils';
7+
8+
export type SessionStatus = 'active' | 'idle' | 'loading';
9+
10+
type TerminalSessionProps = {
11+
isActive: boolean;
12+
isTerminalOpen: boolean;
13+
dimensions: { width: number; height: number };
14+
canCreate: boolean;
15+
canUpdate: boolean;
16+
setFitAddonRef: React.Dispatch<React.SetStateAction<any | null>>;
17+
terminalId: string;
18+
onStatusChange?: (status: SessionStatus) => void;
19+
};
20+
21+
export const TerminalSession: React.FC<TerminalSessionProps> = ({
22+
isActive,
23+
isTerminalOpen,
24+
dimensions,
25+
canCreate,
26+
canUpdate,
27+
setFitAddonRef,
28+
terminalId,
29+
onStatusChange
30+
}) => {
31+
const { terminalRef, fitAddonRef, initializeTerminal, destroyTerminal } = useTerminal(
32+
isTerminalOpen && isActive,
33+
dimensions.width,
34+
dimensions.height,
35+
canCreate || canUpdate,
36+
terminalId
37+
);
38+
39+
const isContainerReady = useContainerReady(
40+
isTerminalOpen && isActive,
41+
terminalRef as React.RefObject<HTMLDivElement>
42+
);
43+
44+
const onStatusChangeRef = useRef(onStatusChange);
45+
onStatusChangeRef.current = onStatusChange;
46+
47+
const hasInitializedRef = useRef(false);
48+
49+
useEffect(() => {
50+
if (isTerminalOpen && isActive && isContainerReady && !hasInitializedRef.current) {
51+
hasInitializedRef.current = true;
52+
onStatusChangeRef.current?.('loading');
53+
initializeTerminal();
54+
const timer = setTimeout(() => onStatusChangeRef.current?.('active'), 500);
55+
return () => clearTimeout(timer);
56+
}
57+
}, [isTerminalOpen, isActive, isContainerReady, initializeTerminal]);
58+
59+
useEffect(() => {
60+
if (fitAddonRef) {
61+
setFitAddonRef(fitAddonRef);
62+
}
63+
}, [fitAddonRef, setFitAddonRef]);
64+
65+
return (
66+
<div
67+
ref={terminalRef}
68+
className={cn(
69+
'flex-1 relative transition-opacity duration-300',
70+
isTerminalOpen && isActive ? 'opacity-100' : 'opacity-0 pointer-events-none'
71+
)}
72+
style={{
73+
visibility: isTerminalOpen && isActive ? 'visible' : 'hidden',
74+
minHeight: '200px',
75+
padding: '8px',
76+
overflow: 'hidden',
77+
backgroundColor: 'var(--terminal-bg)',
78+
scrollbarWidth: 'thin',
79+
height: '100%',
80+
width: '100%',
81+
maxWidth: '100%',
82+
boxSizing: 'border-box',
83+
contain: 'inline-size',
84+
animation: isActive ? 'terminalFadeIn 0.3s ease-out' : 'none'
85+
}}
86+
/>
87+
);
88+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { TerminalSession, type SessionStatus } from './TerminalSession';
2+
export { SessionTab } from './SessionTab';
3+
export { TerminalHeader } from './TerminalHeader';

view/app/terminal/hooks/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { useTerminalSessions } from './useTerminalSessions';
2+
export { useTerminalDimensions } from './useTerminalDimensions';
3+
export { useTerminalStyles } from './useTerminalStyles';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useState, useEffect, useCallback, useRef } from 'react';
2+
3+
export const useTerminalDimensions = (
4+
containerRef: React.RefObject<HTMLDivElement | null>,
5+
isTerminalOpen: boolean
6+
) => {
7+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
8+
const resizeTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
9+
10+
const updateDimensions = useCallback(() => {
11+
if (!containerRef.current) return;
12+
13+
if (resizeTimeoutRef.current) {
14+
clearTimeout(resizeTimeoutRef.current);
15+
}
16+
17+
resizeTimeoutRef.current = setTimeout(() => {
18+
if (containerRef.current) {
19+
setDimensions({
20+
width: containerRef.current.offsetWidth,
21+
height: containerRef.current.offsetHeight
22+
});
23+
}
24+
}, 100);
25+
}, [containerRef]);
26+
27+
useEffect(() => {
28+
if (!containerRef.current) return;
29+
30+
updateDimensions();
31+
32+
const resizeObserver = new ResizeObserver(updateDimensions);
33+
resizeObserver.observe(containerRef.current);
34+
35+
return () => {
36+
resizeObserver.disconnect();
37+
if (resizeTimeoutRef.current) {
38+
clearTimeout(resizeTimeoutRef.current);
39+
}
40+
};
41+
}, [isTerminalOpen, updateDimensions, containerRef]);
42+
43+
return dimensions;
44+
};

0 commit comments

Comments
 (0)