fix: resize cols and rows, not the other way around *sigh*

This commit is contained in:
Aarnav Tale
2025-06-18 13:24:34 -04:00
parent 1e86b0e95b
commit bf1d75a27a
4 changed files with 45 additions and 29 deletions
+8
View File
@@ -23,6 +23,13 @@ export async function loader({
request, request,
context, context,
}: LoaderFunctionArgs<LoadContext>) { }: LoaderFunctionArgs<LoadContext>) {
if (!context.agents?.agentID()) {
throw data(
'WebSSH is only available with the Headplane agent integration',
400,
);
}
const session = await context.sessions.auth(request); const session = await context.sessions.auth(request);
const user = session.get('user'); const user = session.get('user');
if (!user) { if (!user) {
@@ -35,6 +42,7 @@ export async function loader({
'v1/user', 'v1/user',
session.get('api_key')!, session.get('api_key')!,
); );
// MARK: This assumes that a user has authenticated with Headscale first // MARK: This assumes that a user has authenticated with Headscale first
// Since the only way to enforce permissions via ACLs is to generate a // Since the only way to enforce permissions via ACLs is to generate a
// pre-authkey which REQUIRES a user ID, meaning the user has to have // pre-authkey which REQUIRES a user ID, meaning the user has to have
-12
View File
@@ -46,18 +46,6 @@ interface XtermConfig {
OnDisconnect: () => void; OnDisconnect: () => void;
} }
// interface SSHTerminalConfig {
// writeFn: (data: string) => void;
// writeErrorFn: (error: string) => void;
// setReadFn: (cb: (input: string) => void) => void;
// rows: number;
// cols: number;
// timeoutSeconds?: number;
// onConnectionProgress: (message: string) => void;
// onConnected: () => void;
// onDone: () => void;
// }
interface SSHSession { interface SSHSession {
Close(): boolean; Close(): boolean;
Resize(rows: number, cols: number): boolean; Resize(rows: number, cols: number): boolean;
+36 -16
View File
@@ -30,6 +30,17 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
useEffect(() => { useEffect(() => {
pause(); pause();
if (!container.current) {
console.error('Container ref is not set');
return;
}
// Don't create a new terminal if one already exists
if (term.current) {
console.warn('Terminal already exists, skipping initialization');
return;
}
const terminal = new xterm.Terminal({ const terminal = new xterm.Terminal({
allowProposedApi: true, allowProposedApi: true,
cursorBlink: true, cursorBlink: true,
@@ -52,24 +63,23 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
); );
terminal.unicode.activeVersion = '11'; terminal.unicode.activeVersion = '11';
const gl = new WebglAddon();
terminal.loadAddon(gl);
const fit = new FitAddon(); const fit = new FitAddon();
terminal.loadAddon(fit); terminal.loadAddon(fit);
const gl = new WebglAddon();
terminal.loadAddon(gl);
gl.onContextLoss(() => { gl.onContextLoss(() => {
console.warn('WebGL context lost, falling back to canvas rendering'); console.warn('WebGL context lost, falling back to canvas rendering');
gl.dispose(); gl.dispose();
}); });
fit.fit();
term.current = terminal;
terminal.open(container.current!); terminal.open(container.current!);
terminal.focus(); terminal.focus();
term.current = terminal;
let ro: ResizeObserver | null = null;
let onUnload: ((e: Event) => void) | null = null; let onUnload: ((e: Event) => void) | null = null;
const session = ipn.OpenSSH(hostname, username, { const session = ipn.OpenSSH(hostname, username, {
Rows: terminal.rows, Rows: terminal.rows,
Cols: terminal.cols, Cols: terminal.cols,
@@ -103,8 +113,7 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
}, },
}); });
const parent = container.current?.ownerDocument.defaultView ?? window; const ro = new ResizeObserver(() => {
ro = new parent.ResizeObserver(() => {
if (term.current) { if (term.current) {
setIsResizing(true); setIsResizing(true);
fit.fit(); fit.fit();
@@ -112,12 +121,10 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
} }
}); });
if (container.current) { ro.observe(container.current);
ro.observe(container.current);
}
terminal.onResize(({ cols, rows }) => { terminal.onResize(({ cols, rows }) => {
session.Resize(rows, cols); console.log(`Terminal resized to ${cols}x${rows}`);
session.Resize(cols, rows);
}); });
terminal.onData((data) => { terminal.onData((data) => {
@@ -126,12 +133,25 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
onUnload = (_) => session.Close(); onUnload = (_) => session.Close();
parent.addEventListener('unload', onUnload); parent.addEventListener('unload', onUnload);
return () => {
if (onUnload) {
parent.removeEventListener('unload', onUnload);
}
session.Close();
ro?.disconnect();
terminal.dispose();
term.current = null;
inputRef.current = null;
console.log('SSH session closed and terminal disposed');
};
}, []); }, []);
return ( return (
<div className="relative w-full h-full group"> <>
{isLoading ? ( {isLoading ? (
<div className="mx-auto h-screen flex items-center justify-center"> <div className="absolute w-screen z-50 mx-auto h-screen flex items-center justify-center">
<Loader2 className="animate-spin size-10 text-headplane-50" /> <Loader2 className="animate-spin size-10 text-headplane-50" />
</div> </div>
) : undefined} ) : undefined}
@@ -159,6 +179,6 @@ export default function XTerm({ ipn, username, hostname }: XTermProps) {
Failed to connect to SSH session Failed to connect to SSH session
</div> </div>
) : undefined} ) : undefined}
</div> </>
); );
} }
+1 -1
View File
@@ -150,7 +150,7 @@ func (s *SSHSession) Resize(rows, cols int) error {
return nil return nil
} }
return s.Pty.WindowChange(rows, cols) return s.Pty.WindowChange(cols, rows)
} }
// Closes the SSH session. // Closes the SSH session.