1- import { X } from "lucide-react" ;
2- import { useEffect , useMemo , useState } from "react" ;
1+ import { useEffect , useState } from "react" ;
32import { useShallow } from "zustand/react/shallow" ;
43import { useT } from "../../i18n" ;
54import type { Bot , BotFile , Crew } from "../../lib/protocol.gen" ;
65import { useWindowVisible } from "../../shell/visibility" ;
7- import { type BotPanel , routinesOf } from "../../store/app" ;
6+ import { routinesOf } from "../../store/app" ;
87import { useApp } from "../../store/context" ;
9- import { Button } from "../../ui/Button" ;
10- import { Callout } from "../../ui/Callout" ;
118import { PanelClosing , PanelRestored } from "../../ui/panelMotion" ;
12- import { SidePanel } from "../../ui/SidePanel" ;
139import { type Tab , Tabs , tabId } from "../../ui/Tabs" ;
1410import { useStable } from "../../ui/useStable" ;
1511import { BrowserPanel } from "../browser/BrowserPanel" ;
@@ -18,50 +14,30 @@ import { ChatView } from "../chat/ChatView";
1814import { FilesPanel } from "../files/FilesPanel" ;
1915import { ShowFile } from "../files/showFile" ;
2016import { useBotFiles } from "../files/useBotFiles" ;
21- import { SignInButton } from "../onboarding/SignIn" ;
2217import { BotRoutines } from "../routines/RoutineList" ;
2318import { ScreensPanel } from "../screens/ScreensPanel" ;
2419import { ShowScreen } from "../screens/showScreen" ;
2520import { useScreens } from "../screens/useScreens" ;
26- import { AllowRules } from "./AllowRules" ;
21+ import { Dock } from "../terminal/ComputerDock" ;
22+ import { ShowTerminal } from "../terminal/showTerminal" ;
23+ import { TerminalPanel } from "../terminal/TerminalPanel" ;
2724import { BotHeader } from "./BotHeader" ;
25+ import { Details , Notices } from "./BotPanels" ;
2826import { stateView } from "./BotStateBadge" ;
2927import { useFollowBot } from "./useFollowBot" ;
28+ import { usePanel } from "./usePanel" ;
3029
3130type Pane = "chat" | "routines" ;
32- /** What the panel beside the chat shows. */
33- type Side = BotPanel | null ;
3431
3532/**
3633 * A bot's conversation and its routines, with its details in a side panel
3734 * (spec 15.1, 20.9).
3835 */
3936export function BotView ( { bot, crew } : { bot : Bot ; crew : Crew } ) {
4037 const t = useT ( ) ;
41- // Kept per bot in the store, not here: the panel the owner left open is
42- // back when they come back to the bot, and one they closed stays closed
43- // (spec 15.1).
44- const side = useApp ( ( state ) => state . panels [ bot . id ] ?? null ) ;
45- const keepPanel = useApp ( ( state ) => state . setPanel ) ;
46- // The panel that came back is there at once; one opened after it slides.
47- const [ restored , setRestored ] = useState ( side !== null ) ;
48- const setSide = ( panel : Side ) => {
49- setRestored ( false ) ;
50- keepPanel ( bot . id , panel ) ;
51- } ;
52- // The panel on screen: the one open, or the last one while it slides
53- // closed (spec 15.1).
54- const [ leaving , setLeaving ] = useState < Side > ( null ) ;
55- useEffect ( ( ) => {
56- if ( side !== null ) {
57- setLeaving ( side ) ;
58- }
59- } , [ side ] ) ;
60- const beside = side ?? leaving ;
61- const closing = useMemo (
62- ( ) => ( side === null && leaving !== null ? { closed : ( ) => setLeaving ( null ) } : null ) ,
63- [ side , leaving ] ,
64- ) ;
38+ // Kept per bot in the store: the panel the owner left open is back when
39+ // they come back to the bot, and one they closed stays closed (spec 15.1).
40+ const { side, setSide, beside, restored, closing } = usePanel ( bot . id ) ;
6541 const [ pane , setPane ] = useState < Pane > ( "chat" ) ;
6642 const files = useBotFiles ( bot , side === "files" ) ;
6743 // What the owner has seen: files newer than this are new to them.
@@ -152,6 +128,25 @@ export function BotView({ bot, crew }: { bot: Bot; crew: Crew }) {
152128 open : ( panel ) => ( panel === "browser" ? showBrowser ( ) : showScreen ( null ) ) ,
153129 close : ( panel ) => side === panel && setSide ( null ) ,
154130 } ) ;
131+ const showTerminal = useStable ( ( ) => {
132+ if ( filesOpen ) {
133+ seen ( ) ;
134+ }
135+ setSide ( "terminal" ) ;
136+ } ) ;
137+ // The dock of the bot's computer: browser, terminal and files.
138+ const dock = {
139+ open : side ,
140+ pick : ( place : "browser" | "terminal" | "files" ) => {
141+ if ( place === "browser" ) {
142+ showBrowser ( ) ;
143+ } else if ( place === "terminal" ) {
144+ showTerminal ( ) ;
145+ } else if ( ! filesOpen ) {
146+ toggleFiles ( ) ;
147+ }
148+ } ,
149+ } ;
155150 const view = stateView ( bot , crew . paused , t ) ;
156151 const tabs : Tab < Pane > [ ] = [
157152 { id : "chat" , label : t . routines . chatTab } ,
@@ -188,113 +183,48 @@ export function BotView({ bot, crew }: { bot: Bot; crew: Crew }) {
188183 < ShowFile . Provider value = { showFile } >
189184 < ShowBrowser . Provider value = { showBrowser } >
190185 < ShowScreen . Provider value = { showScreen } >
191- < ChatView bot = { bot } stopped = { bot . paused || crew . paused } />
186+ < ShowTerminal . Provider value = { showTerminal } >
187+ < ChatView bot = { bot } stopped = { bot . paused || crew . paused } />
188+ </ ShowTerminal . Provider >
192189 </ ShowScreen . Provider >
193190 </ ShowBrowser . Provider >
194191 </ ShowFile . Provider >
195192 ) : (
196193 < BotRoutines bot = { bot } />
197194 ) }
198195 </ div >
199- < PanelRestored . Provider value = { restored } >
200- < PanelClosing . Provider value = { closing } >
201- { beside === "details" && < Details bot = { bot } onClose = { ( ) => setSide ( null ) } /> }
202- { beside === "browser" && (
203- < BrowserPanel bot = { bot } take = { takeBrowser } onClose = { ( ) => setSide ( null ) } />
204- ) }
205- { beside === "screens" && (
206- < ScreensPanel
207- bot = { bot }
208- data = { screens }
209- path = { screenPath }
210- onPath = { setScreenPath }
211- onClose = { ( ) => setSide ( null ) }
212- />
213- ) }
214- { beside === "files" && (
215- < FilesPanel
216- bot = { bot }
217- data = { files }
218- since = { since }
219- path = { shown }
220- described = { described }
221- onPath = { setShown }
222- onClose = { toggleFiles }
223- />
224- ) }
225- </ PanelClosing . Provider >
226- </ PanelRestored . Provider >
196+ < Dock . Provider value = { dock } >
197+ < PanelRestored . Provider value = { restored } >
198+ < PanelClosing . Provider value = { closing } >
199+ { beside === "details" && < Details bot = { bot } onClose = { ( ) => setSide ( null ) } /> }
200+ { beside === "browser" && (
201+ < BrowserPanel bot = { bot } take = { takeBrowser } onClose = { ( ) => setSide ( null ) } />
202+ ) }
203+ { beside === "screens" && (
204+ < ScreensPanel
205+ bot = { bot }
206+ data = { screens }
207+ path = { screenPath }
208+ onPath = { setScreenPath }
209+ onClose = { ( ) => setSide ( null ) }
210+ />
211+ ) }
212+ { beside === "files" && (
213+ < FilesPanel
214+ bot = { bot }
215+ data = { files }
216+ since = { since }
217+ path = { shown }
218+ described = { described }
219+ onPath = { setShown }
220+ onClose = { toggleFiles }
221+ />
222+ ) }
223+ { beside === "terminal" && < TerminalPanel bot = { bot } onClose = { ( ) => setSide ( null ) } /> }
224+ </ PanelClosing . Provider >
225+ </ PanelRestored . Provider >
226+ </ Dock . Provider >
227227 </ div >
228228 </ section >
229229 ) ;
230230}
231-
232- function Notices ( {
233- bot,
234- crew,
235- view,
236- } : {
237- bot : Bot ;
238- crew : Crew ;
239- view : ReturnType < typeof stateView > ;
240- } ) {
241- const t = useT ( ) ;
242- const notices = [ ] ;
243- if ( crew . paused && ! bot . paused ) {
244- notices . push (
245- < Callout key = "crew" title = { t . bots . notices . crewPaused ( crew . name ) } >
246- { t . bots . notices . crewPausedBody }
247- </ Callout > ,
248- ) ;
249- }
250- // An approval shows in the chat itself; the rest needs a word up here.
251- if ( ( view . tone === "warn" || view . tone === "danger" ) && bot . state !== "needs_approval" ) {
252- notices . push (
253- < Callout key = "state" tone = { view . tone } title = { view . label } >
254- { view . hint }
255- { bot . state === "auth_error" && < SignInButton /> }
256- </ Callout > ,
257- ) ;
258- }
259- if ( notices . length === 0 ) {
260- return null ;
261- }
262- return < div className = "flex flex-col gap-2 border-line border-b px-5 py-3" > { notices } </ div > ;
263- }
264-
265- function Details ( { bot, onClose } : { bot : Bot ; onClose ( ) : void } ) {
266- const words = useT ( ) . bots . details ;
267- return (
268- < SidePanel label = { words . title ( bot . name ) } name = "details" defaultWidth = { 320 } >
269- < header className = "flex h-11 shrink-0 items-center justify-between border-line border-b pr-1.5 pl-4" >
270- < h2 className = "font-semibold text-sm" > { words . title ( bot . name ) } </ h2 >
271- < Button variant = "ghost" size = "sm" icon = { X } label = { words . close } onClick = { onClose } />
272- </ header >
273- < dl className = "flex min-h-0 flex-col gap-4 overflow-y-auto p-4 text-sm" >
274- < div >
275- < dt className = "text-muted text-xs" > { words . role } </ dt >
276- < dd className = "mt-0.5 text-ink-soft" > { bot . role || words . noRole } </ dd >
277- </ div >
278- < div >
279- < dt className = "text-muted text-xs" > { words . folder } </ dt >
280- < dd className = "mt-0.5 break-all font-mono text-xs" data-selectable >
281- { bot . workspace }
282- </ dd >
283- </ div >
284- < div >
285- < dt className = "text-muted text-xs" > { words . process } </ dt >
286- < dd className = "mt-0.5 font-mono text-xs" >
287- { bot . generation === null ? words . notStarted : words . generation ( bot . generation ) }
288- </ dd >
289- </ div >
290- < div >
291- < dt className = "text-muted text-xs" > { words . instructions } </ dt >
292- < dd className = "mt-0.5 whitespace-pre-wrap text-ink-soft" data-selectable >
293- { bot . instructions || words . noInstructions }
294- </ dd >
295- </ div >
296- < AllowRules bot = { bot } />
297- </ dl >
298- </ SidePanel >
299- ) ;
300- }
0 commit comments