44 Desktop ,
55 Folder ,
66 GitFork ,
7+ Globe ,
78 Lightning ,
89 Plus ,
910 Terminal ,
@@ -12,14 +13,24 @@ import {
1213import { isBrainrotCell } from "@posthog/core/command-center/grid" ;
1314import { ANALYTICS_EVENTS , type WorkspaceMode } from "@posthog/shared" ;
1415import type { Task } from "@posthog/shared/domain-types" ;
16+ import {
17+ BrowserPanel ,
18+ useBrowserEnabled ,
19+ } from "@posthog/ui/features/browser/BrowserPanel" ;
1520import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore" ;
1621import { destroyShellTerminal } from "@posthog/ui/features/terminal/destroyShellTerminal" ;
1722import { ShellTerminal } from "@posthog/ui/features/terminal/ShellTerminal" ;
1823import { openTask } from "@posthog/ui/router/useOpenTask" ;
1924import { track } from "@posthog/ui/shell/analytics" ;
2025import { secureRandomString } from "@posthog/ui/utils/random" ;
2126import { Flex , Spinner , Text } from "@radix-ui/themes" ;
22- import { useCallback , useEffect , useRef , useState } from "react" ;
27+ import {
28+ type ReactNode ,
29+ useCallback ,
30+ useEffect ,
31+ useRef ,
32+ useState ,
33+ } from "react" ;
2334import { useFolders } from "../../folders/useFolders" ;
2435import { useCloudPrUrl } from "../../git-interaction/useCloudPrUrl" ;
2536import { useDraftStore } from "../../message-editor/draftStore" ;
@@ -116,11 +127,13 @@ function EmptyCell({ cellIndex }: { cellIndex: number }) {
116127 const assignTask = useCommandCenterStore ( ( s ) => s . assignTask ) ;
117128 const setBrainrotCell = useCommandCenterStore ( ( s ) => s . setBrainrotCell ) ;
118129 const setTerminalCell = useCommandCenterStore ( ( s ) => s . setTerminalCell ) ;
130+ const setBrowserCell = useCommandCenterStore ( ( s ) => s . setBrowserCell ) ;
119131 const startCreating = useCommandCenterStore ( ( s ) => s . startCreating ) ;
120132 const stopCreating = useCommandCenterStore ( ( s ) => s . stopCreating ) ;
121133 const layout = useCommandCenterStore ( ( s ) => s . layout ) ;
122134 const cells = useCommandCenterStore ( ( s ) => s . cells ) ;
123135 const brainrotMode = useSettingsStore ( ( s ) => s . brainrotMode ) ;
136+ const browserEnabled = useBrowserEnabled ( ) ;
124137 const clearDraft = useDraftStore ( ( s ) => s . actions . setDraft ) ;
125138
126139 const sessionId = getCellSessionId ( cellIndex ) ;
@@ -140,6 +153,10 @@ function EmptyCell({ cellIndex }: { cellIndex: number }) {
140153 [ setTerminalCell , cellIndex ] ,
141154 ) ;
142155
156+ const handleNewBrowser = useCallback ( ( ) => {
157+ setBrowserCell ( cellIndex , "about:blank" ) ;
158+ } , [ setBrowserCell , cellIndex ] ) ;
159+
143160 const handleTaskCreated = useCallback (
144161 ( task : Task ) => {
145162 assignTask ( cellIndex , task . id ) ;
@@ -199,6 +216,7 @@ function EmptyCell({ cellIndex }: { cellIndex: number }) {
199216 onOpenChange = { setSelectorOpen }
200217 onNewTask = { ( ) => startCreating ( cellIndex ) }
201218 onNewTerminal = { handleNewTerminal }
219+ onNewBrowser = { browserEnabled ? handleNewBrowser : undefined }
202220 onBrainrot = { brainrotMode ? handleBrainrot : undefined }
203221 >
204222 < button
@@ -279,6 +297,54 @@ function BrainrotCell({ cellIndex }: { cellIndex: number }) {
279297 ) ;
280298}
281299
300+ // Shared chrome for every occupied command-center cell: a titled header with a
301+ // type icon, an optional badge slot, a remove button, and the cell body below.
302+ function CellFrame ( {
303+ icon,
304+ title,
305+ headerExtra,
306+ onRemove,
307+ children,
308+ } : {
309+ icon : ReactNode ;
310+ title : string ;
311+ headerExtra ?: ReactNode ;
312+ onRemove : ( ) => void ;
313+ children : ReactNode ;
314+ } ) {
315+ return (
316+ < Flex direction = "column" height = "100%" >
317+ < Flex
318+ align = "center"
319+ gap = "2"
320+ px = "2"
321+ py = "1"
322+ className = "shrink-0 border-gray-6 border-b"
323+ >
324+ { icon }
325+ < Text
326+ className = "min-w-0 flex-1 truncate font-medium text-[12px]"
327+ title = { title }
328+ >
329+ { title }
330+ </ Text >
331+ { headerExtra }
332+ < button
333+ type = "button"
334+ onClick = { onRemove }
335+ className = "flex h-5 w-5 shrink-0 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
336+ title = "Remove from grid"
337+ >
338+ < X size = { 12 } />
339+ </ button >
340+ </ Flex >
341+ < Flex direction = "column" className = "min-h-0 flex-1" >
342+ { children }
343+ </ Flex >
344+ </ Flex >
345+ ) ;
346+ }
347+
282348function TerminalCell ( {
283349 cellIndex,
284350 terminalId,
@@ -300,37 +366,59 @@ function TerminalCell({
300366 } , [ stateKey , clearCell , cellIndex ] ) ;
301367
302368 return (
303- < Flex direction = "column" height = "100%" >
304- < Flex
305- align = "center"
306- gap = "2"
307- px = "2"
308- py = "1"
309- className = "shrink-0 border-gray-6 border-b"
310- >
311- < Terminal size = { 12 } className = "shrink-0 text-gray-10" />
312- < Text className = "min-w-0 flex-1 truncate font-medium text-[12px]" >
313- Terminal
314- </ Text >
315- { folderName && (
369+ < CellFrame
370+ icon = { < Terminal size = { 12 } className = "shrink-0 text-gray-10" /> }
371+ title = "Terminal"
372+ headerExtra = {
373+ folderName ? (
316374 < span className = "inline-flex items-center gap-0.5 rounded bg-gray-3 px-1 py-0.5 text-[10px] text-gray-10" >
317375 < Folder size = { 10 } />
318376 { folderName }
319377 </ span >
320- ) }
321- < button
322- type = "button"
323- onClick = { handleRemove }
324- className = "flex h-5 w-5 shrink-0 items-center justify-center rounded text-gray-10 transition-colors hover:bg-gray-4 hover:text-gray-12"
325- title = "Remove from grid"
326- >
327- < X size = { 12 } />
328- </ button >
329- </ Flex >
330- < Flex direction = "column" className = "min-h-0 flex-1" >
331- < ShellTerminal cwd = { cwd } stateKey = { stateKey } />
332- </ Flex >
333- </ Flex >
378+ ) : undefined
379+ }
380+ onRemove = { handleRemove }
381+ >
382+ < ShellTerminal cwd = { cwd } stateKey = { stateKey } />
383+ </ CellFrame >
384+ ) ;
385+ }
386+
387+ function hostnameOf ( url : string ) : string | null {
388+ try {
389+ return new URL ( url ) . hostname || null ;
390+ } catch {
391+ return null ;
392+ }
393+ }
394+
395+ function BrowserCell ( { cellIndex, url } : { cellIndex : number ; url : string } ) {
396+ const clearCell = useCommandCenterStore ( ( s ) => s . clearCell ) ;
397+ const updateBrowserCellUrl = useCommandCenterStore (
398+ ( s ) => s . updateBrowserCellUrl ,
399+ ) ;
400+ // Page title once loaded; before that (e.g. a just-restored cell) the
401+ // persisted url's hostname beats a bare "Browser".
402+ const [ title , setTitle ] = useState < string | null > ( null ) ;
403+ const label = title ?? hostnameOf ( url ) ?? "Browser" ;
404+
405+ const onUrlChange = useCallback (
406+ ( next : string ) => updateBrowserCellUrl ( cellIndex , next ) ,
407+ [ updateBrowserCellUrl , cellIndex ] ,
408+ ) ;
409+
410+ return (
411+ < CellFrame
412+ icon = { < Globe size = { 12 } className = "shrink-0 text-gray-10" /> }
413+ title = { label }
414+ onRemove = { ( ) => clearCell ( cellIndex ) }
415+ >
416+ < BrowserPanel
417+ url = { url }
418+ onUrlChange = { onUrlChange }
419+ onTitleChange = { setTitle }
420+ />
421+ </ CellFrame >
334422 ) ;
335423}
336424
@@ -426,6 +514,11 @@ export function CommandCenterPanel({
426514 ) ;
427515 }
428516
517+ // Empty-string url is a valid (blank) browser cell, so check against null.
518+ if ( cell . browserUrl !== null ) {
519+ return < BrowserCell cellIndex = { cell . cellIndex } url = { cell . browserUrl } /> ;
520+ }
521+
429522 if ( ! cell . taskId || ! cell . task ) {
430523 return < EmptyCell cellIndex = { cell . cellIndex } /> ;
431524 }
0 commit comments