11// Copyright 2025, Command Line Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4- import { getTabMetaKeyAtom } from "@/app/store/global" ;
4+ import { WaveAIModel } from "@/app/aipanel/waveai-model" ;
5+ import { getTabMetaKeyAtom , refocusNode } from "@/app/store/global" ;
56import { globalStore } from "@/app/store/jotaiStore" ;
67import * as WOS from "@/app/store/wos" ;
78import { RpcApi } from "@/app/store/wshclientapi" ;
89import { TabRpcClient } from "@/app/store/wshrpcutil" ;
10+ import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks" ;
911import { atoms , isDev } from "@/store/global" ;
12+ import debug from "debug" ;
1013import * as jotai from "jotai" ;
1114import { debounce } from "lodash-es" ;
1215import { ImperativePanelGroupHandle , ImperativePanelHandle } from "react-resizable-panels" ;
1316
17+ const dlog = debug ( "wave:workspace" ) ;
18+
1419const AIPANEL_DEFAULTWIDTH = 300 ;
1520const AIPANEL_MINWIDTH = 250 ;
1621const AIPANEL_MAXWIDTHRATIO = 0.5 ;
1722
1823class WorkspaceLayoutModel {
1924 aiPanelRef : ImperativePanelHandle | null ;
2025 panelGroupRef : ImperativePanelGroupHandle | null ;
21- inResize : boolean ;
26+ panelContainerRef : HTMLDivElement | null ;
27+ aiPanelWrapperRef : HTMLDivElement | null ;
28+ inResize : boolean ; // prevents recursive setLayout calls (setLayout triggers onLayout which calls setLayout)
2229 private aiPanelVisible : boolean ;
2330 private aiPanelWidth : number | null ;
2431 private debouncedPersistWidth : ( width : number ) => void ;
2532 private initialized : boolean = false ;
33+ private transitionTimeoutRef : NodeJS . Timeout | null = null ;
34+ private focusTimeoutRef : NodeJS . Timeout | null = null ;
2635 panelVisibleAtom : jotai . PrimitiveAtom < boolean > ;
2736
2837 constructor ( ) {
2938 this . aiPanelRef = null ;
3039 this . panelGroupRef = null ;
40+ this . panelContainerRef = null ;
41+ this . aiPanelWrapperRef = null ;
3142 this . inResize = false ;
3243 this . aiPanelVisible = isDev ( ) ;
3344 this . aiPanelWidth = null ;
3445 this . panelVisibleAtom = jotai . atom ( this . aiPanelVisible ) ;
3546
47+ this . handleWindowResize = this . handleWindowResize . bind ( this ) ;
48+ this . handlePanelLayout = this . handlePanelLayout . bind ( this ) ;
49+
3650 this . debouncedPersistWidth = debounce ( ( width : number ) => {
3751 try {
3852 RpcApi . SetMetaCommand ( TabRpcClient , {
@@ -77,10 +91,84 @@ class WorkspaceLayoutModel {
7791 return getTabMetaKeyAtom ( this . getTabId ( ) , "waveai:panelwidth" ) ;
7892 }
7993
80- registerRefs ( aiPanelRef : ImperativePanelHandle , panelGroupRef : ImperativePanelGroupHandle ) : void {
94+ registerRefs (
95+ aiPanelRef : ImperativePanelHandle ,
96+ panelGroupRef : ImperativePanelGroupHandle ,
97+ panelContainerRef : HTMLDivElement ,
98+ aiPanelWrapperRef : HTMLDivElement
99+ ) : void {
81100 this . aiPanelRef = aiPanelRef ;
82101 this . panelGroupRef = panelGroupRef ;
102+ this . panelContainerRef = panelContainerRef ;
103+ this . aiPanelWrapperRef = aiPanelWrapperRef ;
83104 this . syncAIPanelRef ( ) ;
105+ this . updateWrapperWidth ( ) ;
106+ }
107+
108+ updateWrapperWidth ( ) : void {
109+ if ( ! this . aiPanelWrapperRef ) {
110+ return ;
111+ }
112+ const width = this . getAIPanelWidth ( ) ;
113+ this . aiPanelWrapperRef . style . width = `${ width } px` ;
114+ }
115+
116+ enableTransitions ( duration : number ) : void {
117+ if ( ! this . panelContainerRef ) {
118+ return ;
119+ }
120+ const panels = this . panelContainerRef . querySelectorAll ( "[data-panel]" ) ;
121+ dlog ( "set transition ease-in-out" , panels ) ;
122+ panels . forEach ( ( panel : HTMLElement ) => {
123+ panel . style . transition = "flex 0.2s ease-in-out" ;
124+ } ) ;
125+
126+ if ( this . transitionTimeoutRef ) {
127+ clearTimeout ( this . transitionTimeoutRef ) ;
128+ }
129+ this . transitionTimeoutRef = setTimeout ( ( ) => {
130+ if ( ! this . panelContainerRef ) {
131+ return ;
132+ }
133+ const panels = this . panelContainerRef . querySelectorAll ( "[data-panel]" ) ;
134+ dlog ( "set transition none" , panels ) ;
135+ panels . forEach ( ( panel : HTMLElement ) => {
136+ panel . style . transition = "none" ;
137+ } ) ;
138+ } , duration ) ;
139+ }
140+
141+ handleWindowResize ( ) : void {
142+ if ( ! this . panelGroupRef ) {
143+ return ;
144+ }
145+ const newWindowWidth = window . innerWidth ;
146+ const aiPanelPercentage = this . getAIPanelPercentage ( newWindowWidth ) ;
147+ const mainContentPercentage = this . getMainContentPercentage ( newWindowWidth ) ;
148+ this . inResize = true ;
149+ const layout = [ aiPanelPercentage , mainContentPercentage ] ;
150+ this . panelGroupRef . setLayout ( layout ) ;
151+ this . inResize = false ;
152+ }
153+
154+ handlePanelLayout ( sizes : number [ ] ) : void {
155+ dlog ( "handlePanelLayout" , "inResize:" , this . inResize , "sizes:" , sizes ) ;
156+ if ( this . inResize ) {
157+ return ;
158+ }
159+ if ( ! this . panelGroupRef ) {
160+ return ;
161+ }
162+
163+ const currentWindowWidth = window . innerWidth ;
164+ const aiPanelPixelWidth = ( sizes [ 0 ] / 100 ) * currentWindowWidth ;
165+ this . handleAIPanelResize ( aiPanelPixelWidth , currentWindowWidth ) ;
166+ const newPercentage = this . getAIPanelPercentage ( currentWindowWidth ) ;
167+ const mainContentPercentage = 100 - newPercentage ;
168+ this . inResize = true ;
169+ const layout = [ newPercentage , mainContentPercentage ] ;
170+ this . panelGroupRef . setLayout ( layout ) ;
171+ this . inResize = false ;
84172 }
85173
86174 syncAIPanelRef ( ) : void {
@@ -125,13 +213,36 @@ class WorkspaceLayoutModel {
125213 if ( ! isDev ( ) && visible ) {
126214 return ;
127215 }
216+ if ( this . focusTimeoutRef != null ) {
217+ clearTimeout ( this . focusTimeoutRef ) ;
218+ this . focusTimeoutRef = null ;
219+ }
128220 this . aiPanelVisible = visible ;
129221 globalStore . set ( this . panelVisibleAtom , visible ) ;
130222 RpcApi . SetMetaCommand ( TabRpcClient , {
131223 oref : WOS . makeORef ( "tab" , this . getTabId ( ) ) ,
132224 meta : { "waveai:panelopen" : visible } ,
133225 } ) ;
226+ this . enableTransitions ( 250 ) ;
134227 this . syncAIPanelRef ( ) ;
228+
229+ if ( visible ) {
230+ this . focusTimeoutRef = setTimeout ( ( ) => {
231+ WaveAIModel . getInstance ( ) . focusInput ( ) ;
232+ this . focusTimeoutRef = null ;
233+ } , 350 ) ;
234+ } else {
235+ const layoutModel = getLayoutModelForStaticTab ( ) ;
236+ const focusedNode = globalStore . get ( layoutModel . focusedNode ) ;
237+ if ( focusedNode == null ) {
238+ layoutModel . focusFirstNode ( ) ;
239+ return ;
240+ }
241+ const blockId = focusedNode ?. data ?. blockId ;
242+ if ( blockId != null ) {
243+ refocusNode ( blockId ) ;
244+ }
245+ }
135246 }
136247
137248 getAIPanelWidth ( ) : number {
@@ -144,6 +255,7 @@ class WorkspaceLayoutModel {
144255
145256 setAIPanelWidth ( width : number ) : void {
146257 this . aiPanelWidth = width ;
258+ this . updateWrapperWidth ( ) ;
147259 this . debouncedPersistWidth ( width ) ;
148260 }
149261
@@ -172,10 +284,6 @@ class WorkspaceLayoutModel {
172284 }
173285 const clampedWidth = this . getClampedAIPanelWidth ( width , windowWidth ) ;
174286 this . setAIPanelWidth ( clampedWidth ) ;
175-
176- if ( ! this . getAIPanelVisible ( ) ) {
177- this . setAIPanelVisible ( true ) ;
178- }
179287 }
180288}
181289
0 commit comments