From e60624386d45c54db48e9bd0aed3bde8c9739f82 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 16:23:04 +0000 Subject: [PATCH 1/2] =?UTF-8?q?refactor(ui):=20split=20Trading=20Accounts?= =?UTF-8?q?=20=C3=97=20Portfolio=20responsibilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trading Accounts (Beta) and Portfolio displayed the same five-account list under two top-level entries, with Trading Accounts re-rendering an aggregate equity banner + per-card sparklines that duplicated Portfolio. Clicking a Trading Accounts card sent users to Portfolio drill-in instead of broker config, blurring the "manage connections" vs "view financial state" line. Disentangle along verbs: - Settings → Trading: broker connection CRUD. UTACard click opens EditUTADialog (was: nav to /uta/:id). Drops PortfolioBanner + sparkline + curve polling; keeps a single per-card equity number as a connection-liveness signal (60s poll vs old 30s). - Portfolio (Beta): aggregate equity + per-UTA drill-in unchanged. Empty state now points to Settings → Trading with an explicit jump button. - Trading-as-Git (Beta): unchanged — orthogonal ops surface. EditUTADialog gains an optional onViewInPortfolio link rendered in the header, so users in config-mode can jump to financial state for the same account without going back through the sidebar. Subsection-header rule for sidebars codified in sections.tsx jsdoc: use headers only when listing items of more than one shape (Portfolio's aggregate + per-instance), not for symmetry across sidebars. Removes the `trading-accounts` ActivitySection / Page / sidebar entry; adds /trading-accounts → /settings/trading redirect. No persistence compat — bump and clear. --- ui/src/App.tsx | 1 - ui/src/components/ActivityBar.tsx | 23 +- ui/src/components/PortfolioSidebar.tsx | 2 +- ui/src/components/SettingsCategoryList.tsx | 21 +- .../components/TradingAccountsBetaSidebar.tsx | 45 --- ui/src/components/uta/EditUTADialog.tsx | 36 ++- ui/src/pages/PortfolioPage.tsx | 28 +- ui/src/pages/TradingPage.tsx | 288 ++++++------------ ui/src/sections.tsx | 13 +- ui/src/tabs/UrlAdopter.tsx | 1 + ui/src/tabs/registry.tsx | 2 +- ui/src/tabs/types.ts | 1 - 12 files changed, 171 insertions(+), 290 deletions(-) delete mode 100644 ui/src/components/TradingAccountsBetaSidebar.tsx diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b2821c3bf..5b347b2ed 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -21,7 +21,6 @@ export type Page = | 'trading-as-git' | 'settings' | 'dev' | 'traditional-chat' | 'notifications-legacy' | 'connectors-legacy' - | 'trading-accounts' /** Track whether we're at a desktop viewport (md+ in Tailwind = ≥768px). */ function useIsDesktop(): boolean { diff --git a/ui/src/components/ActivityBar.tsx b/ui/src/components/ActivityBar.tsx index e1d72023c..44dae75e2 100644 --- a/ui/src/components/ActivityBar.tsx +++ b/ui/src/components/ActivityBar.tsx @@ -1,4 +1,4 @@ -import { type LucideIcon, MessageSquare, MessagesSquare, Inbox, Bell, LineChart, GitBranch, BarChart3, Newspaper, Zap, Settings, Code2, TerminalSquare, ChevronDown, Plug, Landmark, Info } from 'lucide-react' +import { type LucideIcon, MessageSquare, MessagesSquare, Inbox, Bell, LineChart, GitBranch, BarChart3, Newspaper, Zap, Settings, Code2, TerminalSquare, ChevronDown, Plug, Info } from 'lucide-react' import { useState } from 'react' import { type Page } from '../App' import { useWorkspace } from '../tabs/store' @@ -25,7 +25,6 @@ function activitySectionFor(page: Page): ActivitySection { case 'traditional-chat': return 'traditional-chat' case 'notifications-legacy': return 'notifications-legacy' case 'connectors-legacy': return 'connectors-legacy' - case 'trading-accounts': return 'trading-accounts' } } @@ -101,20 +100,18 @@ const NAV_SECTIONS: NavSection[] = [ { page: 'news', label: 'News', icon: Newspaper, defaultTab: { kind: 'news', params: {} } }, ], }, - // Beta — functional but unstable. Goal: unified abstraction across - // broker accounts (Trading Accounts) + the Trading-as-Git workflow - // + the Portfolio view that surfaces it. Large engineering ahead, - // no fixed timeline — configurable today, but lock-in cost can - // change as the abstraction settles. Default-expanded because the - // items here are actively useful; the Beta label is the right - // amount of caution, not a hide. + // Beta — functional but unstable. The underlying cross-broker + // unification (UTA abstraction, FX/options/futures) is in active + // rearchitecture. Portfolio surfaces that state; Trading-as-Git is + // the operations side (pending broker writes). Broker connection + // CRUD lives under Settings → Trading, not here — it's a config + // surface, not a state/ops one. { sectionLabel: 'Beta', - description: 'Goal here is a unified abstraction across broker accounts (deposit/withdraw, options, futures, FX). Large engineering effort, no fixed timeline. Configure and try, but don\'t depend on schema or UX as stable yet.', + description: 'Cross-broker unified state + ops surfaces. The abstraction underneath is still being settled — try them, but don\'t depend on schema or UX as stable yet. Broker connection setup lives in Settings → Trading.', items: [ - { page: 'trading-accounts', label: 'Trading Accounts', icon: Landmark, defaultTab: { kind: 'settings', params: { category: 'trading' } } }, - { page: 'trading-as-git', label: 'Trading as Git', icon: GitBranch }, - { page: 'portfolio', label: 'Portfolio', icon: LineChart, defaultTab: { kind: 'portfolio', params: {} } }, + { page: 'trading-as-git', label: 'Trading as Git', icon: GitBranch }, + { page: 'portfolio', label: 'Portfolio', icon: LineChart, defaultTab: { kind: 'portfolio', params: {} } }, ], }, { diff --git a/ui/src/components/PortfolioSidebar.tsx b/ui/src/components/PortfolioSidebar.tsx index 3f6206bcf..aa2c39377 100644 --- a/ui/src/components/PortfolioSidebar.tsx +++ b/ui/src/components/PortfolioSidebar.tsx @@ -39,7 +39,7 @@ export function PortfolioSidebar() {

Loading…

) : utas.length === 0 ? (

- No accounts yet. Add one in Settings → Trading Accounts. + No accounts yet. Add one in Settings → Trading.

) : ( utas.map((uta) => { diff --git a/ui/src/components/SettingsCategoryList.tsx b/ui/src/components/SettingsCategoryList.tsx index 37295fcf8..539f86466 100644 --- a/ui/src/components/SettingsCategoryList.tsx +++ b/ui/src/components/SettingsCategoryList.tsx @@ -7,24 +7,16 @@ type SettingsCategory = Extract['params']['categ interface CategoryItem { label: string category: SettingsCategory - /** - * Other view kinds that count as "active" for this row. Used by - * Trading Accounts: when a uta-detail tab is focused, Trading - * Accounts should still light up. - */ - alsoActiveFor?: ViewSpec['kind'][] } const CATEGORIES: CategoryItem[] = [ - { label: 'General', category: 'general' }, - { label: 'AI Provider', category: 'ai-provider' }, - // Trading Accounts moved to its own ActivityBar Beta entry — see - // TradingAccountsBetaSidebar. The `settings/trading` ViewSpec is - // still the underlying tab. + { label: 'General', category: 'general' }, + { label: 'AI Provider', category: 'ai-provider' }, + { label: 'Trading', category: 'trading' }, // Connectors moved to its own ActivityBar Legacy entry — see // ConnectorsLegacySidebar. - { label: 'MCP Server', category: 'mcp' }, - { label: 'Market Data', category: 'market-data' }, + { label: 'MCP Server', category: 'mcp' }, + { label: 'Market Data', category: 'market-data' }, { label: 'News Sources', category: 'news-collector' }, ] @@ -41,8 +33,7 @@ export function SettingsCategoryList() {
{CATEGORIES.map((item) => { const active = - (focused?.kind === 'settings' && focused.params.category === item.category) || - (item.alsoActiveFor != null && focused != null && item.alsoActiveFor.includes(focused.kind)) + focused?.kind === 'settings' && focused.params.category === item.category return ( getFocusedTab(state)?.spec) - // Also light up when a `uta-detail` tab is focused — that's the - // sibling view that gets opened from inside the TradingPage. - const isActive = - (focused?.kind === 'settings' && focused.params.category === 'trading') - || focused?.kind === 'uta-detail' - const openOrFocus = useWorkspace((state) => state.openOrFocus) - - return ( -
-
- - - Trading Accounts - - } - active={isActive} - onClick={() => - openOrFocus({ kind: 'settings', params: { category: 'trading' } }) - } - /> -
-
- ) -} diff --git a/ui/src/components/uta/EditUTADialog.tsx b/ui/src/components/uta/EditUTADialog.tsx index 18d9c0f7a..9dbd22dd9 100644 --- a/ui/src/components/uta/EditUTADialog.tsx +++ b/ui/src/components/uta/EditUTADialog.tsx @@ -11,15 +11,21 @@ import { SchemaFormFields } from './SchemaFormFields' /** * UTA configuration dialog — edits credentials, guards, enabled state. - * Mounted from both the trading page (legacy entry) and the UTA detail - * page (new entry, accessed via the Edit button in the page header). + * Mounted from Settings → Trading (primary CRUD entry) and from the UTA + * detail page in Portfolio (sibling Edit button). + * + * When opened from Settings → Trading, the parent passes `onViewInPortfolio` + * to render a header link that switches the user over to the Portfolio + * drill-in for this account. When opened from inside Portfolio's detail + * page, that prop is omitted (the user is already in that context). */ -export function EditUTADialog({ uta, preset, health, onSave, onDelete, onClose }: { +export function EditUTADialog({ uta, preset, health, onSave, onDelete, onViewInPortfolio, onClose }: { uta: UTAConfig preset?: BrokerPreset health?: BrokerHealthInfo onSave: (a: UTAConfig) => Promise onDelete: () => Promise + onViewInPortfolio?: () => void onClose: () => void }) { const [draft, setDraft] = useState(uta) @@ -74,11 +80,25 @@ export function EditUTADialog({ uta, preset, health, onSave, onDelete, onClose }

{uta.id}

- +
+ {onViewInPortfolio && ( + + )} + +
{/* Body */} diff --git a/ui/src/pages/PortfolioPage.tsx b/ui/src/pages/PortfolioPage.tsx index e169b1a79..05a42910e 100644 --- a/ui/src/pages/PortfolioPage.tsx +++ b/ui/src/pages/PortfolioPage.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { api, type Position, type WalletCommitLog, type EquityCurvePoint, type UTASnapshotSummary } from '../api' import { useAutoSave } from '../hooks/useAutoSave' import { useAccountHealth } from '../hooks/useAccountHealth' +import { useWorkspace } from '../tabs/store' import { PageHeader } from '../components/PageHeader' import { EmptyState } from '../components/StateViews' import { EquityCurve } from '../components/EquityCurve' @@ -277,7 +278,7 @@ export function PortfolioPage() { {/* Empty states */} {data.accounts.length === 0 && !loading && ( - + )} {data.accounts.length > 0 && allPositions.length === 0 && !loading && ( @@ -334,6 +335,31 @@ async function fetchPortfolioData(): Promise { } } +// ==================== Empty: no trading accounts ==================== + +function NoAccountsEmpty() { + const openOrFocus = useWorkspace((s) => s.openOrFocus) + const setSidebar = useWorkspace((s) => s.setSidebar) + const goToTradingSettings = () => { + setSidebar('settings') + openOrFocus({ kind: 'settings', params: { category: 'trading' } }) + } + return ( +
+

No trading accounts connected.

+

+ Portfolio shows live equity, positions and PnL across all your brokers. Add a connection to get started. +

+ +
+ ) +} + // ==================== Hero Metrics ==================== function HeroMetrics({ equity, curve }: { diff --git a/ui/src/pages/TradingPage.tsx b/ui/src/pages/TradingPage.tsx index ab7da0ac4..e44e53e10 100644 --- a/ui/src/pages/TradingPage.tsx +++ b/ui/src/pages/TradingPage.tsx @@ -1,5 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from 'react' -import { useNavigate } from 'react-router-dom' +import { useState, useEffect, useMemo } from 'react' import { Field, inputClass } from '../components/form' import { SDKSelector } from '../components/SDKSelector' import type { SDKOption } from '../components/SDKSelector' @@ -10,13 +9,18 @@ import { PageHeader } from '../components/PageHeader' import { Dialog } from '../components/uta/Dialog' import { HealthBadge } from '../components/uta/HealthBadge' import { SchemaFormFields } from '../components/uta/SchemaFormFields' -import { Metric, signFromDelta } from '../components/Metric' -import { Sparkline } from '../components/Sparkline' -import { fmt, fmtPnl, fmtPctSigned } from '../lib/format' +import { EditUTADialog } from '../components/uta/EditUTADialog' +import { fmt } from '../lib/format' import { api } from '../api' -import type { UTAConfig, BrokerPreset, BrokerHealthInfo, TestConnectionResult, Position, AccountInfo, EquityCurvePoint } from '../api/types' +import { useWorkspace } from '../tabs/store' +import type { UTAConfig, BrokerPreset, BrokerHealthInfo, TestConnectionResult, Position, AccountInfo } from '../api/types' // ==================== Live equity (across all UTAs) ==================== +// +// TradingPage is the CRUD surface for broker connections. The single +// per-card equity number is here as a liveness signal — "this connection +// returned real account data" — not as a portfolio view. Aggregate +// equity, sparklines, 24h deltas, and trade logs live in Portfolio. interface EquitySummary { totalEquity: string @@ -26,105 +30,50 @@ interface EquitySummary { accounts: Array<{ id: string; label: string; equity: string; cash: string }> } -interface PerUtaCurve { values: number[]; firstAtCutoff: number | null; latest: number | null } - -interface CurveSummary { - /** Aggregate (across all UTAs) — feeds the hero banner. */ - total: { values: number[]; firstAtCutoff: number | null; latest: number | null } - /** Per-UTA curves — feed the per-card sparkline + 24h delta. */ - perUta: Record -} - -const CUTOFF_24H_MS = 24 * 60 * 60 * 1000 - -/** Build a curve summary from equity-curve points: latest value + the - * oldest value still within the trailing 24h window (the "baseline" - * for today PnL). */ -function summarizeCurve(points: EquityCurvePoint[]): CurveSummary { - const sorted = [...points].sort((a, b) => a.timestamp.localeCompare(b.timestamp)) - const cutoff = Date.now() - CUTOFF_24H_MS - - const totalValues: number[] = [] - let totalFirstAtCutoff: number | null = null - let totalLatest: number | null = null - const perUtaValues = new Map() - const perUtaFirstAtCutoff = new Map() - const perUtaLatest = new Map() - - for (const p of sorted) { - const t = new Date(p.timestamp).getTime() - const totalN = Number(p.equity) - if (Number.isFinite(totalN)) { - totalValues.push(totalN) - totalLatest = totalN - if (t >= cutoff && totalFirstAtCutoff == null) totalFirstAtCutoff = totalN - } - for (const [id, raw] of Object.entries(p.accounts ?? {})) { - const n = Number(raw) - if (!Number.isFinite(n)) continue - let arr = perUtaValues.get(id) - if (!arr) { arr = []; perUtaValues.set(id, arr) } - arr.push(n) - perUtaLatest.set(id, n) - if (t >= cutoff && !perUtaFirstAtCutoff.has(id)) perUtaFirstAtCutoff.set(id, n) - } - } - - const perUta: Record = {} - for (const [id, values] of perUtaValues) { - perUta[id] = { - values, - firstAtCutoff: perUtaFirstAtCutoff.get(id) ?? null, - latest: perUtaLatest.get(id) ?? null, - } - } - - return { - total: { values: totalValues, firstAtCutoff: totalFirstAtCutoff, latest: totalLatest }, - perUta, - } -} - // ==================== Page ==================== export function TradingPage() { const tc = useTradingConfig() const healthMap = useAccountHealth() - const navigate = useNavigate() + const openOrFocus = useWorkspace((s) => s.openOrFocus) + const setSidebar = useWorkspace((s) => s.setSidebar) const [showAdd, setShowAdd] = useState(false) + const [editingId, setEditingId] = useState(null) const [presets, setPresets] = useState([]) const [equity, setEquity] = useState(null) - const [curve, setCurve] = useState(null) const [lastUpdated, setLastUpdated] = useState(null) useEffect(() => { api.trading.getBrokerPresets().then(r => setPresets(r.presets)).catch(() => {}) }, []) - // Live aggregates: pull `equity()` for headline numbers and `equityCurve()` - // for trend + 24h delta. One fetch each per cycle, shared across the - // hero banner + every UTA card. Polling cadence (30s) is informational — - // user can drill into a UTA for the 15s refresh of broker state. - const refreshAggregates = useCallback(async () => { - try { - const [eq, cv] = await Promise.all([ - api.trading.equity().catch(() => null), - api.trading.equityCurve({ limit: 1500 }).catch(() => ({ points: [] as EquityCurvePoint[] })), - ]) - if (eq) setEquity(eq) - setCurve(summarizeCurve(cv.points)) - setLastUpdated(new Date()) - } catch { - // Don't surface — aggregates are nice-to-have, the page still renders - // from useTradingConfig if the equity endpoint is down. + // Per-card liveness signal — `equity()` lets each card show "this + // connection actually returned an account balance" rather than just + // "ping went through". 60s cadence is enough; trend/sparkline/aggregate + // moved to Portfolio. + useEffect(() => { + let cancelled = false + const refresh = async () => { + const eq = await api.trading.equity().catch(() => null) + if (cancelled) return + if (eq) { + setEquity(eq) + setLastUpdated(new Date()) + } } + refresh() + const id = setInterval(refresh, 60_000) + return () => { cancelled = true; clearInterval(id) } }, []) - useEffect(() => { - refreshAggregates() - const id = setInterval(refreshAggregates, 30_000) - return () => clearInterval(id) - }, [refreshAggregates]) + const editingUTA = editingId ? tc.utas.find(u => u.id === editingId) : null + const editingPreset = editingUTA ? presets.find(p => p.id === editingUTA.presetId) : undefined + + const openInPortfolio = (id: string) => { + setEditingId(null) + setSidebar('portfolio') + openOrFocus({ kind: 'uta-detail', params: { id } }) + } if (tc.loading) return if (tc.error) { @@ -149,32 +98,27 @@ export function TradingPage() { {tc.utas.length === 0 ? ( setShowAdd(true)} /> ) : ( - <> - {equity && } - -
- {tc.utas.map((uta) => { - const equityRow = equity?.accounts.find(a => a.id === uta.id) ?? null - return ( - p.id === uta.presetId)} - health={healthMap[uta.id]} - equity={equityRow} - curve={curve?.perUta[uta.id] ?? null} - onClick={() => navigate(`/uta/${uta.id}`)} - /> - ) - })} - -
- +
+ {tc.utas.map((uta) => { + const equityRow = equity?.accounts.find(a => a.id === uta.id) ?? null + return ( + p.id === uta.presetId)} + health={healthMap[uta.id]} + equity={equityRow} + onClick={() => setEditingId(uta.id)} + /> + ) + })} + +
)} @@ -190,16 +134,31 @@ export function TradingPage() { } setShowAdd(false) // Trigger a fresh fetch so the new UTA shows live numbers right away. - void refreshAggregates() + void api.trading.equity().then(setEquity).catch(() => {}) return created }} onOpenExisting={(id) => { setShowAdd(false) - navigate(`/uta/${id}`) + setEditingId(id) }} onClose={() => setShowAdd(false)} /> )} + + {editingUTA && ( + { await tc.saveUTA(next) }} + onDelete={async () => { + await tc.deleteUTA(editingUTA.id) + setEditingId(null) + }} + onViewInPortfolio={() => openInPortfolio(editingUTA.id)} + onClose={() => setEditingId(null)} + /> + )} ) } @@ -233,48 +192,6 @@ function EmptyState({ onAdd }: { onAdd: () => void }) { // ==================== Portfolio banner (hero) ==================== -function PortfolioBanner({ equity, curve }: { - equity: EquitySummary - curve: { values: number[]; firstAtCutoff: number | null; latest: number | null } | null -}) { - const total = Number(equity.totalEquity) - const cash = Number(equity.totalCash) - const unrealized = Number(equity.totalUnrealizedPnL) - - // 24h delta from the curve summary. If curve is empty or the cutoff - // baseline isn't available (UTA freshly added), suppress the delta. - let deltaNode: React.ReactNode = null - if (curve && curve.latest != null && curve.firstAtCutoff != null) { - const delta = curve.latest - curve.firstAtCutoff - const pct = curve.firstAtCutoff !== 0 ? (delta / curve.firstAtCutoff) * 100 : 0 - const sign = signFromDelta(delta) - const arrow = sign === 'up' ? '▲' : sign === 'down' ? '▼' : '·' - const color = sign === 'up' ? 'text-green' : sign === 'down' ? 'text-red' : 'text-text-muted' - deltaNode = ( - - {arrow} {fmtPnl(delta, 'USD')} ({fmtPctSigned(pct)}) today - - ) - } - - return ( -
-

Total Portfolio · USD

-
- - {fmt(total, 'USD')} - - {deltaNode} -
-
- Cash {fmt(cash, 'USD')} - · - Unrealized = 0 ? 'text-green' : 'text-red'}`}>{fmtPnl(unrealized, 'USD')} -
-
- ) -} - // ==================== Subtitle builder ==================== function buildSubtitle(uta: UTAConfig, preset?: BrokerPreset): string { @@ -300,12 +217,11 @@ function buildSubtitle(uta: UTAConfig, preset?: BrokerPreset): string { // ==================== UTA Card ==================== -function UTACard({ uta, preset, health, equity, curve, onClick }: { +function UTACard({ uta, preset, health, equity, onClick }: { uta: UTAConfig preset?: BrokerPreset health?: BrokerHealthInfo equity?: { equity: string; cash: string } | null - curve?: PerUtaCurve | null onClick: () => void }) { const isDisabled = health?.disabled || uta.enabled === false @@ -313,23 +229,21 @@ function UTACard({ uta, preset, health, equity, curve, onClick }: { ? { text: preset.badge, color: `${preset.badgeColor} ${preset.badgeColor.replace('text-', 'bg-')}/10` } : { text: uta.presetId.slice(0, 2).toUpperCase(), color: 'text-text-muted bg-text-muted/10' } - // 24h delta for this UTA. - const delta = curve && curve.latest != null && curve.firstAtCutoff != null - ? { value: curve.latest - curve.firstAtCutoff, pct: curve.firstAtCutoff !== 0 ? ((curve.latest - curve.firstAtCutoff) / curve.firstAtCutoff) * 100 : 0 } - : null - - const sparkValues = curve?.values ?? [] - const showSpark = !isDisabled && sparkValues.length >= 2 - + // Per-card equity is a liveness signal, not a portfolio view — + // proves the connection returned a real account balance, not just + // a ping. Aggregate / curves / per-account drill-in all live in + // Portfolio. const equityNum = equity ? Number(equity.equity) : null - const cashNum = equity ? Number(equity.cash) : null + const equityNode = !isDisabled && equityNum != null && Number.isFinite(equityNum) + ? {fmt(equityNum, 'USD')} + : null return ( ) } diff --git a/ui/src/sections.tsx b/ui/src/sections.tsx index 4201ba08f..ad48f2873 100644 --- a/ui/src/sections.tsx +++ b/ui/src/sections.tsx @@ -9,6 +9,14 @@ * Routes have moved to tabs/UrlAdopter.tsx (URL → spec adoption) and * tabs/registry.tsx (spec → URL projection). This file is now just the * activity-section → sidebar lookup. + * + * Subsection-header convention: a sidebar uses subsection headers (e.g. + * Portfolio's "Overview" / "Accounts (N)") IF AND ONLY IF it lists items + * of more than one shape — typically an aggregate view alongside per- + * instance rows. Sidebars listing one kind of thing (Settings categories, + * Workspace instances, Market list, Chat channels) do NOT use headers; + * adding them for symmetry would perform a categorization that isn't in + * the underlying data. Portfolio is the only sidebar that qualifies today. */ import type { ComponentType } from 'react' @@ -16,7 +24,6 @@ import { ChatChannelListContainer } from './components/ChatChannelListContainer' import { TraditionalChatSidebar } from './components/TraditionalChatSidebar' import { NotificationsLegacySidebar } from './components/NotificationsLegacySidebar' import { ConnectorsLegacySidebar } from './components/ConnectorsLegacySidebar' -import { TradingAccountsBetaSidebar } from './components/TradingAccountsBetaSidebar' import { NewChannelButton } from './components/NewChannelButton' import { InboxSidebar } from './components/InboxSidebar' import { WorkspacesSidebar } from './components/workspace/WorkspacesSidebar' @@ -98,10 +105,6 @@ const SECTION_BY_KEY: Record = { title: 'Connectors', Secondary: ConnectorsLegacySidebar, }, - 'trading-accounts': { - title: 'Trading Accounts', - Secondary: TradingAccountsBetaSidebar, - }, } /** Resolve the sidebar config for the currently selected ActivitySection. */ diff --git a/ui/src/tabs/UrlAdopter.tsx b/ui/src/tabs/UrlAdopter.tsx index 034a82a85..b23eb1303 100644 --- a/ui/src/tabs/UrlAdopter.tsx +++ b/ui/src/tabs/UrlAdopter.tsx @@ -86,6 +86,7 @@ export function UrlAdopter() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/tabs/registry.tsx b/ui/src/tabs/registry.tsx index 1213b1bd7..f35b29930 100644 --- a/ui/src/tabs/registry.tsx +++ b/ui/src/tabs/registry.tsx @@ -124,7 +124,7 @@ const settingsCategoryTitle: Record< > = { general: 'Settings', 'ai-provider': 'AI Provider', - trading: 'Trading Accounts', + trading: 'Trading', connectors: 'Connectors', mcp: 'MCP Server', 'market-data': 'Market Data', diff --git a/ui/src/tabs/types.ts b/ui/src/tabs/types.ts index b639b548e..62cffec13 100644 --- a/ui/src/tabs/types.ts +++ b/ui/src/tabs/types.ts @@ -56,7 +56,6 @@ export type ActivitySection = | 'traditional-chat' | 'notifications-legacy' | 'connectors-legacy' - | 'trading-accounts' export interface Tab { id: string From 1388442feb03858fe2ebcf073f303003162aaa72 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 17:07:33 +0000 Subject: [PATCH 2/2] fix(ui): activate matching sidebar on URL adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: fresh-load / deep-link / back-forward to a deep URL only opened the tab; selectedSidebar stayed at whatever was persisted (or null on first run), leaving the secondary sidebar column blank. First-time users landing on /inbox saw ActivityBar + main panel with empty middle space — no left-rail context. This got more visible after Portfolio became a more common bookmark target post-IA- refactor. Add specToSection(spec) and call setSidebar inside useAdopt so URL- driven navigation lands with the matching sidebar already open. Only fires from real URL events (mount + popstate); silent UrlSync writes via replaceState don't re-trigger it, so explicit user sidebar choices still win. uta-detail maps to portfolio (not settings) — its URL lives under /settings/uta/:id for historical reasons but the page is a Portfolio drill-in. --- ui/src/tabs/UrlAdopter.tsx | 39 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/ui/src/tabs/UrlAdopter.tsx b/ui/src/tabs/UrlAdopter.tsx index b23eb1303..52fcb59ce 100644 --- a/ui/src/tabs/UrlAdopter.tsx +++ b/ui/src/tabs/UrlAdopter.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { Navigate, Route, Routes, useParams } from 'react-router-dom' import { useWorkspace } from './store' -import { specEquals, type ViewSpec } from './types' +import { specEquals, type ActivitySection, type ViewSpec } from './types' import { getView } from './registry' /** @@ -196,15 +196,50 @@ function SetSidebarOnly({ section }: { section: import('./types').ActivitySectio return null } +/** + * Map a ViewSpec to the ActivitySection whose sidebar should accompany + * it. URL adoption uses this so a fresh page load / deep link / browser + * back-forward lands on a screen with the matching sidebar already + * open — otherwise `selectedSidebar` stays at whatever was persisted + * (or null on first run), and the page renders without left context. + * + * `uta-detail` is intentionally Portfolio's sidebar: the URL lives + * under /settings/uta/:id for historical reasons but the page is a + * Portfolio drill-in (positions / equity for one account). + */ +function specToSection(spec: ViewSpec): ActivitySection { + switch (spec.kind) { + case 'inbox': return 'inbox' + case 'chat': return 'traditional-chat' + case 'workspace': + case 'workspace-list': + case 'template-catalog': + case 'template-detail': return 'workspaces' + case 'portfolio': + case 'uta-detail': return 'portfolio' + case 'automation': return 'automation' + case 'news': return 'news' + case 'market-list': + case 'market-detail': return 'market' + case 'settings': return 'settings' + case 'dev': return 'dev' + case 'notifications-inbox': return 'notifications-legacy' + } +} + /** * Compare focused tab against `spec` and openOrFocus only if different — - * skips redundant store updates on every render. + * skips redundant store updates on every render. Also activates the + * matching sidebar so URL-driven navigation (fresh load, deep link, + * back-forward) lands with the expected left-rail context, not blank. */ function useAdopt(spec: ViewSpec) { const openOrFocus = useWorkspace((state) => state.openOrFocus) + const setSidebar = useWorkspace((state) => state.setSidebar) // Stable string key for dep tracking; spec is freshly built each render. const key = `${spec.kind}:${JSON.stringify(spec.params)}` useEffect(() => { + setSidebar(specToSection(spec)) const state = useWorkspace.getState() const focused = state.tree.kind === 'leaf' && state.tree.group.activeTabId ? state.tabs[state.tree.group.activeTabId]