@@ -19,16 +19,18 @@ interface ResultPanelProps {
1919 diagnostics : Diagnostic [ ] ;
2020 ast ?: string ;
2121 astTree ?: ASTNode ;
22+ tsAstTree ?: ASTNode ;
2223 initialized ?: boolean ;
2324 error ?: string ;
2425 fixedCode ?: string ;
2526 typeInfo ?: string ;
2627 loading ?: boolean ;
2728 onAstNodeSelect ?: ( start : number , end : number ) => void ;
2829 selectedAstNodeRange ?: { start : number ; end : number } ;
30+ onRequestTsAst ?: ( ) => void ;
2931}
3032
31- type TabType = 'lint' | 'fixed' | 'ast' | 'type' ;
33+ type TabType = 'lint' | 'fixed' | 'ast' | 'ast_ts' | ' type';
3234
3335interface ASTNode {
3436 type : string ;
@@ -44,13 +46,15 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
4446 diagnostics,
4547 ast,
4648 astTree,
49+ tsAstTree,
4750 error,
4851 initialized,
4952 fixedCode,
5053 typeInfo,
5154 loading,
5255 onAstNodeSelect,
5356 selectedAstNodeRange,
57+ onRequestTsAst,
5458 } = props ;
5559 const [ activeTab , setActiveTab ] = useState < TabType > ( ( ) => {
5660 if ( typeof window === 'undefined' ) return 'lint' ;
@@ -60,7 +64,13 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
6064 const hashParams = new URLSearchParams ( window . location . hash . slice ( 1 ) ) ;
6165 tab = hashParams . get ( 'tab' ) ;
6266 }
63- if ( tab === 'lint' || tab === 'ast' || tab === 'fixed' || tab === 'type' ) {
67+ if (
68+ tab === 'lint' ||
69+ tab === 'ast' ||
70+ tab === 'ast_ts' ||
71+ tab === 'fixed' ||
72+ tab === 'type'
73+ ) {
6474 return tab as TabType ;
6575 }
6676 return 'lint' ;
@@ -81,6 +91,13 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
8191 }
8292 } , [ activeTab ] ) ;
8393
94+ // Notify parent when TS AST tab is opened (including initial state)
95+ useEffect ( ( ) => {
96+ if ( activeTab === 'ast_ts' ) {
97+ onRequestTsAst ?.( ) ;
98+ }
99+ } , [ activeTab ] ) ;
100+
84101 // Respond to browser navigation updating the tab
85102 useEffect ( ( ) => {
86103 if ( typeof window === 'undefined' ) return ;
@@ -95,6 +112,7 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
95112 if (
96113 tab === 'lint' ||
97114 tab === 'ast' ||
115+ tab === 'ast_ts' ||
98116 tab === 'fixed' ||
99117 tab === 'type'
100118 ) {
@@ -108,9 +126,12 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
108126 return ( ) => window . removeEventListener ( 'popstate' , handler ) ;
109127 } , [ ] ) ;
110128
111- // AST tree view state
129+ // AST tree view state (tsgo)
112130 const [ expanded , setExpanded ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
113131 const [ selectedId , setSelectedId ] = useState < string | null > ( null ) ;
132+ // AST tree view state (TypeScript)
133+ const [ tsExpanded , setTsExpanded ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
134+ const [ tsSelectedId , setTsSelectedId ] = useState < string | null > ( null ) ;
114135
115136 function nodeId ( n : ASTNode ) {
116137 return `${ n . type } :${ n . start } -${ n . end } ` ;
@@ -171,6 +192,54 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
171192 ) ;
172193 } ;
173194
195+ // TypeScript AST rendering (separate selection/expansion state)
196+ function tsNodeId ( n : ASTNode ) {
197+ return `ts:${ n . type } :${ n . start } -${ n . end } ` ;
198+ }
199+ function renderTsNode ( n : ASTNode , depth = 0 ) {
200+ const id = tsNodeId ( n ) ;
201+ const open = tsExpanded . has ( id ) ;
202+ const hasKids = isExpandable ( n ) ;
203+ const preview = n . text ? n . text . replace ( / \s + / g, ' ' ) . slice ( 0 , 40 ) : '' ;
204+ return (
205+ < div key = { id } className = "ast-node" style = { { paddingLeft : depth * 2 } } >
206+ < div
207+ className = { `ast-node-row ${ tsSelectedId === id ? 'selected' : '' } ` }
208+ onClick = { ( ) => {
209+ setTsSelectedId ( id ) ;
210+ onAstNodeSelect ?.( n . start , n . end ) ;
211+ } }
212+ >
213+ { hasKids && (
214+ < button
215+ className = { `twisty ${ open ? 'open' : '' } ` }
216+ onClick = { e => {
217+ e . stopPropagation ( ) ;
218+ setTsExpanded ( prev => {
219+ const next = new Set ( prev ) ;
220+ if ( next . has ( id ) ) next . delete ( id ) ;
221+ else next . add ( id ) ;
222+ return next ;
223+ } ) ;
224+ } }
225+ aria-label = { open ? 'Collapse' : 'Expand' }
226+ />
227+ ) }
228+ < span className = "node-type" > { n . type } </ span >
229+ < span className = "node-range" >
230+ [{ n . start } , { n . end } ]
231+ </ span >
232+ { preview && < span className = "node-preview" > “{ preview } ”</ span > }
233+ </ div >
234+ { open && hasKids && (
235+ < div className = "ast-children" >
236+ { n . children ! . map ( child => renderTsNode ( child , depth + 1 ) ) }
237+ </ div >
238+ ) }
239+ </ div >
240+ ) ;
241+ }
242+
174243 // When selection in editor changes, select smallest covering AST node and expand its ancestors
175244 useEffect ( ( ) => {
176245 if ( ! astTree || ! selectedAstNodeRange ) return ;
@@ -200,17 +269,52 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
200269 }
201270 } , [ selectedAstNodeRange , astTree ] ) ;
202271
203- // Auto-expand root when a new tree arrives
272+ // Auto-expand roots when new trees arrive
204273 useEffect ( ( ) => {
205- if ( ! astTree ) return ;
206- const id = nodeId ( astTree ) ;
207- setExpanded ( prev => {
208- if ( prev . has ( id ) ) return prev ;
209- const next = new Set ( prev ) ;
210- next . add ( id ) ;
211- return next ;
212- } ) ;
213- } , [ astTree ] ) ;
274+ if ( astTree ) {
275+ const id = nodeId ( astTree ) ;
276+ setExpanded ( prev => {
277+ if ( prev . has ( id ) ) return prev ;
278+ const next = new Set ( prev ) ;
279+ next . add ( id ) ;
280+ return next ;
281+ } ) ;
282+ }
283+ if ( tsAstTree ) {
284+ const id = tsNodeId ( tsAstTree ) ;
285+ setTsExpanded ( prev => {
286+ if ( prev . has ( id ) ) return prev ;
287+ const next = new Set ( prev ) ;
288+ next . add ( id ) ;
289+ return next ;
290+ } ) ;
291+ }
292+ } , [ astTree , tsAstTree ] ) ;
293+
294+ // Selection sync for TS AST
295+ useEffect ( ( ) => {
296+ if ( ! tsAstTree || ! selectedAstNodeRange ) return ;
297+ const { start, end } = selectedAstNodeRange ;
298+ let best : { node : ASTNode ; depth : number ; path : ASTNode [ ] } | null = null ;
299+ function visit ( node : ASTNode , depth : number , path : ASTNode [ ] ) {
300+ if ( node . start <= start && node . end >= end ) {
301+ if ( ! best || depth > best . depth )
302+ best = { node, depth, path : [ ...path , node ] } ;
303+ if ( node . children )
304+ for ( const c of node . children ) visit ( c , depth + 1 , [ ...path , node ] ) ;
305+ }
306+ }
307+ visit ( tsAstTree , 0 , [ ] ) ;
308+ if ( best ) {
309+ const id = tsNodeId ( best . node ) ;
310+ setTsSelectedId ( id ) ;
311+ setTsExpanded ( prev => {
312+ const next = new Set ( prev ) ;
313+ for ( const p of best ! . path ) next . add ( tsNodeId ( p ) ) ;
314+ return next ;
315+ } ) ;
316+ }
317+ } , [ selectedAstNodeRange , tsAstTree ] ) ;
214318
215319 // Share button state and handler
216320 const [ shareCopied , setShareCopied ] = useState ( false ) ;
@@ -245,7 +349,19 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
245349 onClick = { ( ) => setActiveTab ( 'ast' ) }
246350 aria-pressed = { activeTab === 'ast' }
247351 >
248- AST
352+ AST (tsgo)
353+ </ Button >
354+ < Button
355+ type = "button"
356+ variant = { activeTab === 'ast_ts' ? 'default' : 'outline' }
357+ size = "sm"
358+ onClick = { ( ) => {
359+ setActiveTab ( 'ast_ts' ) ;
360+ onRequestTsAst ?.( ) ;
361+ } }
362+ aria-pressed = { activeTab === 'ast_ts' }
363+ >
364+ AST (TypeScript)
249365 </ Button >
250366 </ div >
251367 < div className = "result-actions" >
@@ -320,6 +436,22 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => {
320436 ) }
321437 </ div >
322438 ) }
439+
440+ { ! error && activeTab === 'ast_ts' && (
441+ < div className = "ast-view" >
442+ { tsAstTree ? (
443+ < div className = "ast-tree" role = "tree" >
444+ { renderTsNode ( tsAstTree ) }
445+ </ div >
446+ ) : (
447+ < div className = "empty-state" >
448+ < div className = "empty-text" >
449+ TypeScript AST will be displayed here
450+ </ div >
451+ </ div >
452+ ) }
453+ </ div >
454+ ) }
323455 </ div >
324456 ) : (
325457 < div className = "result-content" >
0 commit comments