1
1
import type { Task , Terminal , WebSocketSession } from '@codesandbox/sdk' ;
2
+ import { FitAddon } from '@xterm/addon-fit' ;
2
3
import { Terminal as XTerm } from '@xterm/xterm' ;
3
4
import { v4 as uuidv4 } from 'uuid' ;
4
5
import type { ErrorManager } from '../error' ;
@@ -16,6 +17,7 @@ export interface CLISession {
16
17
// Task is readonly
17
18
task : Task | null ;
18
19
xterm : XTerm ;
20
+ fitAddon : FitAddon ;
19
21
}
20
22
21
23
export interface TaskSession extends CLISession {
@@ -33,6 +35,7 @@ export class CLISessionImpl implements CLISession {
33
35
terminal : Terminal | null ;
34
36
task : Task | null ;
35
37
xterm : XTerm ;
38
+ fitAddon : FitAddon ;
36
39
37
40
constructor (
38
41
public readonly name : string ,
@@ -41,7 +44,9 @@ export class CLISessionImpl implements CLISession {
41
44
private readonly errorManager : ErrorManager ,
42
45
) {
43
46
this . id = uuidv4 ( ) ;
47
+ this . fitAddon = new FitAddon ( ) ;
44
48
this . xterm = this . createXTerm ( ) ;
49
+ this . xterm . loadAddon ( this . fitAddon ) ;
45
50
this . terminal = null ;
46
51
this . task = null ;
47
52
@@ -67,7 +72,22 @@ export class CLISessionImpl implements CLISession {
67
72
this . xterm . onData ( ( data : string ) => {
68
73
terminal . write ( data ) ;
69
74
} ) ;
75
+
76
+ // Handle terminal resize
77
+ this . xterm . onResize ( ( { cols, rows } ) => {
78
+ // Check if terminal has resize method
79
+ if ( 'resize' in terminal && typeof terminal . resize === 'function' ) {
80
+ terminal . resize ( cols , rows ) ;
81
+ }
82
+ } ) ;
83
+
70
84
await terminal . open ( ) ;
85
+
86
+ // Set initial terminal size and environment
87
+ if ( this . xterm . cols && this . xterm . rows && 'resize' in terminal && typeof terminal . resize === 'function' ) {
88
+ terminal . resize ( this . xterm . cols , this . xterm . rows ) ;
89
+ }
90
+
71
91
} catch ( error ) {
72
92
console . error ( 'Failed to initialize terminal:' , error ) ;
73
93
this . terminal = null ;
@@ -91,16 +111,51 @@ export class CLISessionImpl implements CLISession {
91
111
}
92
112
93
113
createXTerm ( ) {
94
- return new XTerm ( {
114
+ const terminal = new XTerm ( {
95
115
cursorBlink : true ,
96
116
fontSize : 12 ,
97
117
fontFamily : 'monospace' ,
98
- convertEol : true ,
118
+ convertEol : false ,
99
119
allowTransparency : true ,
100
120
disableStdin : false ,
101
121
allowProposedApi : true ,
102
122
macOptionIsMeta : true ,
123
+ altClickMovesCursor : false ,
124
+ windowsMode : false ,
125
+ scrollback : 1000 ,
126
+ screenReaderMode : false ,
127
+ fastScrollModifier : 'alt' ,
128
+ fastScrollSensitivity : 5 ,
103
129
} ) ;
130
+
131
+ // Override write method to handle Claude Code's redrawing patterns
132
+ const originalWrite = terminal . write . bind ( terminal ) ;
133
+ terminal . write = ( data : string | Uint8Array , callback ?: ( ) => void ) => {
134
+ if ( typeof data === 'string' ) {
135
+ // Detect Claude Code's redraw pattern: multiple line clears with cursor movement
136
+ const lineUpPattern = / ( \x1b \[ 2 K \x1b \[ 1 A ) + \x1b \[ 2 K \x1b \[ G / ;
137
+ if ( lineUpPattern . test ( data ) ) {
138
+ // Count how many lines are being cleared
139
+ const matches = data . match ( / \x1b \[ 1 A / g) ;
140
+ const lineCount = matches ? matches . length : 0 ;
141
+
142
+ // Clear the number of lines being redrawn plus some buffer
143
+ for ( let i = 0 ; i <= lineCount + 2 ; i ++ ) {
144
+ terminal . write ( '\x1b[2K\x1b[1A\x1b[2K' ) ;
145
+ }
146
+ terminal . write ( '\x1b[G' ) ; // Go to beginning of line
147
+
148
+ // Extract just the content after the clearing commands
149
+ const contentMatch = data . match ( / \x1b \[ G ( .+ ) $ / s) ;
150
+ if ( contentMatch && contentMatch [ 1 ] ) {
151
+ return originalWrite ( contentMatch [ 1 ] , callback ) ;
152
+ }
153
+ }
154
+ }
155
+ return originalWrite ( data , callback ) ;
156
+ } ;
157
+
158
+ return terminal ;
104
159
}
105
160
106
161
async createDevTaskTerminal ( ) {
0 commit comments