@@ -10,15 +10,18 @@ import {
1010 ExpandableMenuItemRightElement ,
1111} from "@/element/expandablemenu" ;
1212import { Popover , PopoverButton , PopoverContent } from "@/element/popover" ;
13- import { fireAndForget , makeIconClass , useAtomValueSafe } from "@/util/util" ;
13+ import { RpcApi } from "@/app/store/wshclientapi" ;
14+ import { TabRpcClient } from "@/app/store/wshrpcutil" ;
15+ import { fireAndForget , makeIconClass , shellQuote , stringToBase64 , useAtomValueSafe } from "@/util/util" ;
1416import clsx from "clsx" ;
15- import { atom , PrimitiveAtom , useAtom , useAtomValue , useSetAtom } from "jotai" ;
17+ import { Atom , atom , PrimitiveAtom , useAtom , useAtomValue , useSetAtom } from "jotai" ;
1618import { splitAtom } from "jotai/utils" ;
1719import { OverlayScrollbarsComponent } from "overlayscrollbars-react" ;
18- import { CSSProperties , forwardRef , useCallback , useEffect } from "react" ;
20+ import { CSSProperties , forwardRef , useCallback , useEffect , useMemo , useRef } from "react" ;
21+ import { debounce } from "throttle-debounce" ;
1922import WorkspaceSVG from "../asset/workspace.svg" ;
2023import { IconButton } from "../element/iconbutton" ;
21- import { atoms , getApi } from "../store/global" ;
24+ import { atoms , getAllBlockComponentModels , getApi , globalStore } from "../store/global" ;
2225import { WorkspaceService } from "../store/services" ;
2326import { getObjectValue , makeORef } from "../store/wos" ;
2427import { waveEventSubscribe } from "../store/wps" ;
@@ -84,7 +87,7 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, ref) => {
8487
8588 const saveWorkspace = ( ) => {
8689 fireAndForget ( async ( ) => {
87- await WorkspaceService . UpdateWorkspace ( activeWorkspace . oid , "" , "" , "" , true ) ;
90+ await WorkspaceService . UpdateWorkspace ( activeWorkspace . oid , "" , "" , "" , "" , true ) ;
8891 await updateWorkspaceList ( ) ;
8992 setEditingWorkspace ( activeWorkspace . oid ) ;
9093 } ) ;
@@ -138,6 +141,60 @@ const WorkspaceSwitcher = forwardRef<HTMLDivElement>((_, ref) => {
138141 ) ;
139142} ) ;
140143
144+ interface BlockAwareViewModel extends ViewModel {
145+ blockId : string ;
146+ blockAtom : Atom < Block > ;
147+ }
148+
149+ interface PreviewViewModel extends BlockAwareViewModel {
150+ goHistory : ( path : string ) => Promise < void > ;
151+ }
152+
153+ /**
154+ * Type guard that checks if a ViewModel has block awareness (blockId and blockAtom properties).
155+ */
156+ function isBlockAwareViewModel ( viewModel : ViewModel ) : viewModel is BlockAwareViewModel {
157+ return "blockId" in viewModel && "blockAtom" in viewModel ;
158+ }
159+
160+ /**
161+ * Type guard that checks if a ViewModel is a preview view with navigation capabilities.
162+ */
163+ function isPreviewViewModel ( viewModel : ViewModel ) : viewModel is PreviewViewModel {
164+ return viewModel . viewType === "preview" && isBlockAwareViewModel ( viewModel ) && "goHistory" in viewModel ;
165+ }
166+
167+ /**
168+ * Updates all local blocks to use a new workspace directory.
169+ * For preview blocks, navigates to the new directory.
170+ * For terminal blocks, sends a cd command to change to the new directory.
171+ * Skips blocks that have a remote connection.
172+ */
173+ async function updateBlocksWithNewDirectory ( newDirectory : string ) : Promise < void > {
174+ const allModels = getAllBlockComponentModels ( ) ;
175+ for ( const model of allModels ) {
176+ if ( model ?. viewModel == null ) {
177+ continue ;
178+ }
179+ const viewModel = model . viewModel ;
180+ if ( ! isBlockAwareViewModel ( viewModel ) ) {
181+ continue ;
182+ }
183+ const blockData = globalStore . get ( viewModel . blockAtom ) ;
184+ if ( blockData ?. meta ?. connection ) {
185+ continue ;
186+ }
187+ if ( isPreviewViewModel ( viewModel ) ) {
188+ await viewModel . goHistory ( newDirectory ) ;
189+ } else if ( viewModel . viewType === "term" ) {
190+ RpcApi . ControllerInputCommand ( TabRpcClient , {
191+ blockid : viewModel . blockId ,
192+ inputdata64 : stringToBase64 ( `cd ${ shellQuote ( newDirectory ) } \n` ) ,
193+ } ) ;
194+ }
195+ }
196+ }
197+
141198const WorkspaceSwitcherItem = ( {
142199 entryAtom,
143200 onDeleteWorkspace,
@@ -152,20 +209,47 @@ const WorkspaceSwitcherItem = ({
152209 const workspace = workspaceEntry . workspace ;
153210 const isCurrentWorkspace = activeWorkspace . oid === workspace . oid ;
154211
155- const setWorkspace = useCallback ( ( newWorkspace : Workspace ) => {
156- setWorkspaceEntry ( { ...workspaceEntry , workspace : newWorkspace } ) ;
157- if ( newWorkspace . name != "" ) {
158- fireAndForget ( ( ) =>
159- WorkspaceService . UpdateWorkspace (
160- workspace . oid ,
161- newWorkspace . name ,
162- newWorkspace . icon ,
163- newWorkspace . color ,
164- false
165- )
166- ) ;
167- }
168- } , [ ] ) ;
212+ const pendingDirectoryRef = useRef < string | null > ( null ) ;
213+
214+ const debouncedBlockUpdate = useMemo (
215+ ( ) =>
216+ debounce ( 300 , ( newDirectory : string ) => {
217+ pendingDirectoryRef . current = null ;
218+ fireAndForget ( async ( ) => {
219+ await updateBlocksWithNewDirectory ( newDirectory ) ;
220+ } ) ;
221+ } ) ,
222+ [ ]
223+ ) ;
224+
225+ const setWorkspace = useCallback (
226+ ( newWorkspace : Workspace ) => {
227+ setWorkspaceEntry ( ( prev ) => {
228+ const oldDirectory = prev . workspace . directory ;
229+ const newDirectory = newWorkspace . directory ;
230+ const directoryChanged = newDirectory !== oldDirectory ;
231+
232+ if ( newWorkspace . name !== "" ) {
233+ fireAndForget ( async ( ) => {
234+ await WorkspaceService . UpdateWorkspace (
235+ prev . workspace . oid ,
236+ newWorkspace . name ,
237+ newWorkspace . icon ,
238+ newWorkspace . color ,
239+ newWorkspace . directory ?? "" ,
240+ false
241+ ) ;
242+ } ) ;
243+ if ( directoryChanged && isCurrentWorkspace && newDirectory ) {
244+ pendingDirectoryRef . current = newDirectory ;
245+ debouncedBlockUpdate ( newDirectory ) ;
246+ }
247+ }
248+ return { ...prev , workspace : newWorkspace } ;
249+ } ) ;
250+ } ,
251+ [ debouncedBlockUpdate , isCurrentWorkspace , setWorkspaceEntry ]
252+ ) ;
169253
170254 const isActive = ! ! workspaceEntry . windowId ;
171255 const editIconDecl : IconButtonDecl = {
@@ -233,10 +317,12 @@ const WorkspaceSwitcherItem = ({
233317 title = { workspace . name }
234318 icon = { workspace . icon }
235319 color = { workspace . color }
320+ directory = { workspace . directory ?? "" }
236321 focusInput = { isEditing }
237322 onTitleChange = { ( title ) => setWorkspace ( { ...workspace , name : title } ) }
238323 onColorChange = { ( color ) => setWorkspace ( { ...workspace , color } ) }
239324 onIconChange = { ( icon ) => setWorkspace ( { ...workspace , icon } ) }
325+ onDirectoryChange = { ( directory ) => setWorkspace ( { ...workspace , directory } ) }
240326 onDeleteWorkspace = { ( ) => onDeleteWorkspace ( workspace . oid ) }
241327 />
242328 </ ExpandableMenuItem >
0 commit comments