@@ -13,6 +13,7 @@ import { WebLinksAddon } from "xterm-addon-web-links";
13
13
import { WebglAddon } from "xterm-addon-webgl" ;
14
14
import { deploymentConfig } from "api/queries/deployment" ;
15
15
import { workspaceByOwnerAndName } from "api/queries/workspaces" ;
16
+ import type { WorkspaceAgent } from "api/typesGenerated" ;
16
17
import { useProxy } from "contexts/ProxyContext" ;
17
18
import { ThemeOverride } from "contexts/ThemeProvider" ;
18
19
import themes from "theme" ;
@@ -34,6 +35,8 @@ export const Language = {
34
35
websocketErrorMessagePrefix : "WebSocket failed: " ,
35
36
} ;
36
37
38
+ type TerminalState = "connected" | "disconnected" | "initializing" ;
39
+
37
40
const TerminalPage : FC = ( ) => {
38
41
// Maybe one day we'll support a light themed terminal, but terminal coloring
39
42
// is notably a pain because of assumptions certain programs might make about your
@@ -45,9 +48,8 @@ const TerminalPage: FC = () => {
45
48
const username = params . username . replace ( "@" , "" ) ;
46
49
const xtermRef = useRef < HTMLDivElement > ( null ) ;
47
50
const [ terminal , setTerminal ] = useState < XTerm . Terminal | null > ( null ) ;
48
- const [ terminalState , setTerminalState ] = useState <
49
- "connected" | "disconnected" | "initializing"
50
- > ( "initializing" ) ;
51
+ const [ terminalState , setTerminalState ] =
52
+ useState < TerminalState > ( "initializing" ) ;
51
53
const [ searchParams ] = useSearchParams ( ) ;
52
54
const isDebugging = searchParams . has ( "debug" ) ;
53
55
// The reconnection token is a unique token that identifies
@@ -67,12 +69,6 @@ const TerminalPage: FC = () => {
67
69
const selectedProxy = proxy . proxy ;
68
70
const latency = selectedProxy ? proxyLatencies [ selectedProxy . id ] : undefined ;
69
71
70
- const lifecycleState = workspaceAgent ?. lifecycle_state ;
71
- const prevLifecycleState = useRef ( lifecycleState ) ;
72
- useEffect ( ( ) => {
73
- prevLifecycleState . current = lifecycleState ;
74
- } , [ lifecycleState ] ) ;
75
-
76
72
const config = useQuery ( deploymentConfig ( ) ) ;
77
73
const renderer = config . data ?. config . web_terminal_renderer ;
78
74
@@ -95,6 +91,7 @@ const TerminalPage: FC = () => {
95
91
} , [ handleWebLink ] ) ;
96
92
97
93
// Create the terminal!
94
+ const fitAddonRef = useRef < FitAddon > ( ) ;
98
95
useEffect ( ( ) => {
99
96
if ( ! xtermRef . current || config . isLoading ) {
100
97
return ;
@@ -115,6 +112,7 @@ const TerminalPage: FC = () => {
115
112
terminal . loadAddon ( new CanvasAddon ( ) ) ;
116
113
}
117
114
const fitAddon = new FitAddon ( ) ;
115
+ fitAddonRef . current = fitAddon ;
118
116
terminal . loadAddon ( fitAddon ) ;
119
117
terminal . loadAddon ( new Unicode11Addon ( ) ) ;
120
118
terminal . unicode . activeVersion = "11" ;
@@ -303,11 +301,13 @@ const TerminalPage: FC = () => {
303
301
</ title >
304
302
</ Helmet >
305
303
< div css = { { display : "flex" , flexDirection : "column" , height : "100vh" } } >
306
- { lifecycleState === "start_error" && < ErrorScriptAlert /> }
307
- { lifecycleState === "starting" && < LoadingScriptsAlert /> }
308
- { lifecycleState === "ready" &&
309
- prevLifecycleState . current === "starting" && < LoadedScriptsAlert /> }
310
- { terminalState === "disconnected" && < DisconnectedAlert /> }
304
+ < TerminalAlerts
305
+ agent = { workspaceAgent }
306
+ state = { terminalState }
307
+ onAlertChange = { ( ) => {
308
+ fitAddonRef . current ?. fit ( ) ;
309
+ } }
310
+ />
311
311
< div css = { styles . terminal } ref = { xtermRef } data-testid = "terminal" />
312
312
</ div >
313
313
@@ -328,6 +328,62 @@ const TerminalPage: FC = () => {
328
328
) ;
329
329
} ;
330
330
331
+ type TerminalAlertsProps = {
332
+ agent : WorkspaceAgent | undefined ;
333
+ state : TerminalState ;
334
+ onAlertChange : ( ) => void ;
335
+ } ;
336
+
337
+ const TerminalAlerts = ( {
338
+ agent,
339
+ state,
340
+ onAlertChange,
341
+ } : TerminalAlertsProps ) => {
342
+ const lifecycleState = agent ?. lifecycle_state ;
343
+ const prevLifecycleState = useRef ( lifecycleState ) ;
344
+ useEffect ( ( ) => {
345
+ prevLifecycleState . current = lifecycleState ;
346
+ } , [ lifecycleState ] ) ;
347
+
348
+ // We want to observe the children of the wrapper to detect when the alert
349
+ // changes. So the terminal page can resize itself.
350
+ //
351
+ // Would it be possible to just always call fit() when this component
352
+ // re-renders instead of using an observer?
353
+ //
354
+ // This is a good question and the why this does not work is that the .fit()
355
+ // needs to run after the render so in this case, I just think the mutation
356
+ // observer is more reliable. I could use some hacky setTimeout inside of
357
+ // useEffect to do that, I guess, but I don't think it would be any better.
358
+ const wrapperRef = useRef < HTMLDivElement > ( null ) ;
359
+ useEffect ( ( ) => {
360
+ if ( ! wrapperRef . current ) {
361
+ return ;
362
+ }
363
+ const observer = new MutationObserver ( onAlertChange ) ;
364
+ observer . observe ( wrapperRef . current , { childList : true } ) ;
365
+
366
+ return ( ) => {
367
+ observer . disconnect ( ) ;
368
+ } ;
369
+ } , [ onAlertChange ] ) ;
370
+
371
+ return (
372
+ < div ref = { wrapperRef } >
373
+ { state === "disconnected" ? (
374
+ < DisconnectedAlert />
375
+ ) : lifecycleState === "start_error" ? (
376
+ < ErrorScriptAlert />
377
+ ) : lifecycleState === "starting" ? (
378
+ < LoadingScriptsAlert />
379
+ ) : lifecycleState === "ready" &&
380
+ prevLifecycleState . current === "starting" ? (
381
+ < LoadedScriptsAlert />
382
+ ) : null }
383
+ </ div >
384
+ ) ;
385
+ } ;
386
+
331
387
const styles = {
332
388
terminal : ( theme ) => ( {
333
389
width : "100%" ,
0 commit comments