11import { useCallback , useEffect , useState } from 'react'
2- import { Link , useLocation , useNavigate , useParams } from 'react-router-dom'
2+ import { Link , useLocation , useNavigate , useParams , useSearchParams } from 'react-router-dom'
3+ import { BOT_TABS , TAB_LABEL , botPath , parseBotTab , type BotTab } from '../botRoutes'
34import { ApiError , api } from '../api'
45import {
56 Banner ,
@@ -11,6 +12,7 @@ import {
1112 Tag ,
1213 Warnings ,
1314} from '../components/ui'
15+ import BotSwitcher from '../components/BotSwitcher'
1416import ComposeExportLink from '../components/ComposeExportLink'
1517import LogViewer from '../components/LogViewer'
1618import OneShotRunner from '../components/OneShotRunner'
@@ -23,20 +25,10 @@ import { formatTimestamp, shortAddress, shortImage } from '../format'
2325import { confirmRemovePlan } from '../removeBot'
2426import type { Bot , ConfigBody , MigrationResult , UpdatesStatus } from '../types'
2527
26- const TABS = [ 'settings' , 'dashboard' , 'config' , 'logs' , 'tools' ] as const
27- type Tab = ( typeof TABS ) [ number ]
28-
29- const TAB_LABEL : Record < Tab , string > = {
30- settings : 'Settings' ,
31- dashboard : 'Dashboard' ,
32- config : 'Raw config' ,
33- logs : 'Logs' ,
34- tools : 'Tools' ,
35- }
36-
3728export default function BotDetail ( ) {
3829 const { name = '' } = useParams ( )
3930 const navigate = useNavigate ( )
31+ const [ searchParams , setSearchParams ] = useSearchParams ( )
4032 const [ bot , setBot ] = useState < Bot | null > ( null )
4133 const [ error , setError ] = useState < string | null > ( null )
4234 // The wizard redirects here with what it just did, so its confirmation survives
@@ -54,16 +46,32 @@ export default function BotDetail() {
5446 )
5547 const [ busy , setBusy ] = useState < string | null > ( null )
5648 // After create, land on Tools so Approve allowances is the next obvious step.
57- const [ tab , setTab ] = useState < Tab > ( ( ) =>
58- handoff ?. needsPermit2 ? 'tools' : 'settings' ,
59- )
49+ // Tab lives in `?tab=` so switching bots from the title keeps the same section.
50+ const fallbackTab : BotTab = handoff ?. needsPermit2 ? 'tools' : 'settings'
51+ const tab = parseBotTab ( searchParams . get ( 'tab' ) , fallbackTab )
6052 const [ updates , setUpdates ] = useState < UpdatesStatus | null > ( null )
6153
62- const load = useCallback ( async ( ) => {
54+ useEffect ( ( ) => {
55+ const raw = searchParams . get ( 'tab' )
56+ if ( raw === tab ) return
57+ setSearchParams (
58+ ( prev ) => {
59+ const next = new URLSearchParams ( prev )
60+ next . set ( 'tab' , tab )
61+ return next
62+ } ,
63+ { replace : true } ,
64+ )
65+ } , [ searchParams , setSearchParams , tab ] )
66+
67+ const load = useCallback ( async ( signal ?: { cancelled : boolean } ) => {
6368 try {
64- setBot ( await api . bot ( name ) )
69+ const next = await api . bot ( name )
70+ if ( signal ?. cancelled ) return
71+ setBot ( next )
6572 setError ( null )
6673 } catch ( e ) {
74+ if ( signal ?. cancelled ) return
6775 setError ( e instanceof ApiError ? e . message : String ( e ) )
6876 }
6977 } , [ name ] )
@@ -78,8 +86,12 @@ export default function BotDetail() {
7886 } , [ ] )
7987
8088 useEffect ( ( ) => {
81- void load ( )
89+ const signal = { cancelled : false }
90+ void load ( signal )
8291 void loadUpdates ( )
92+ return ( ) => {
93+ signal . cancelled = true
94+ }
8395 } , [ load , loadUpdates ] )
8496
8597 const botUpdate = updates ?. bots . find ( ( b ) => b . name === name )
@@ -152,7 +164,7 @@ export default function BotDetail() {
152164 < Link to = "/" className = "text-sm text-muted hover:text-ink" >
153165 ← Fleet
154166 </ Link >
155- < h1 className = "text-xl font-bold" > { bot . name } </ h1 >
167+ < BotSwitcher name = { bot . name } / >
156168 < StatePill state = { bot . state } status = { bot . status } />
157169 { bot . config ?. corridorLabel && (
158170 < span className = "text-sm text-muted" > { bot . config . corridorLabel } </ span >
@@ -213,13 +225,13 @@ export default function BotDetail() {
213225 </ p >
214226 < p >
215227 Open{ ' ' }
216- < button
217- type = "button"
228+ < Link
229+ to = { botPath ( name , 'tools' ) }
230+ replace
218231 className = "font-bold underline hover:no-underline"
219- onClick = { ( ) => setTab ( 'tools' ) }
220232 >
221233 Tools → Approve allowances
222- </ button >
234+ </ Link >
223235 , then dry-run, then Start.
224236 </ p >
225237 </ div >
@@ -347,19 +359,20 @@ export default function BotDetail() {
347359 className = "flex flex-nowrap gap-x-0.5 overflow-x-auto border-b border-line-soft [scrollbar-width:none] [-ms-overflow-style:none] sm:gap-x-1 [&::-webkit-scrollbar]:hidden"
348360 aria-label = "Bot sections"
349361 >
350- { TABS . map ( ( t ) => (
351- < button
362+ { BOT_TABS . map ( ( t ) => (
363+ < Link
352364 key = { t }
353- type = "button"
354- onClick = { ( ) => setTab ( t ) }
365+ to = { botPath ( name , t ) }
366+ replace
367+ aria-current = { tab === t ? 'page' : undefined }
355368 className = { `-mb-px shrink-0 border-b-2 px-2.5 py-2 text-xs font-bold transition sm:px-3 sm:text-sm ${
356369 tab === t
357370 ? 'border-accent text-ink'
358371 : 'border-transparent text-muted hover:text-ink'
359372 } `}
360373 >
361374 { TAB_LABEL [ t ] }
362- </ button >
375+ </ Link >
363376 ) ) }
364377 </ nav >
365378 < div
0 commit comments