| author | |
| committer | |
| log | c2f8cb7ca1576afd70294b98dfe1de9be17aacae |
| tree | 123db90d8b341c6193b38ca055763a732e897f09 |
| parent | 89ee692bf0436505d008c1d70215e70836eba4e2 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>3 files changed, 48 insertions(+), 3 deletions(-)
apps/web/src/components/ChatView.tsx+7| ... | @@ -5913,6 +5913,11 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -5913,6 +5913,11 @@ function ChatViewContent(props: ChatViewProps) { |
| 5913 | rightPanelAvailable={activeProject !== null} | 5913 | rightPanelAvailable={activeProject !== null} |
| 5914 | rightPanelOpen={rightPanelOpen} | 5914 | rightPanelOpen={rightPanelOpen} |
| 5915 | rightPanelShortcutLabel={shortcutLabelForCommand(keybindings, "rightPanel.toggle")} | 5915 | rightPanelShortcutLabel={shortcutLabelForCommand(keybindings, "rightPanel.toggle")} |
| 5916 | // Suppressed while the Agents surface is visible: the roster itself is | ||
| 5917 | // on screen, so the toggle badge would be pointing at nothing. | ||
| 5918 | liveAgentCount={ | ||
| 5919 | rightPanelOpen && activeRightPanelSurface?.kind === "agents" ? 0 : agentPanelModel.liveCount | ||
| 5920 | } | ||
| 5916 | onToggleTerminal={toggleTerminalVisibility} | 5921 | onToggleTerminal={toggleTerminalVisibility} |
| 5917 | onToggleRightPanel={toggleRightPanel} | 5922 | onToggleRightPanel={toggleRightPanel} |
| 5918 | /> | 5923 | /> |
| ... | @@ -6419,6 +6424,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -6419,6 +6424,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 6419 | browserAvailable={isPreviewSupportedInRuntime()} | 6424 | browserAvailable={isPreviewSupportedInRuntime()} |
| 6420 | diffAvailable={isServerThread && isGitRepo} | 6425 | diffAvailable={isServerThread && isGitRepo} |
| 6421 | filesAvailable={activeProject !== null} | 6426 | filesAvailable={activeProject !== null} |
| 6427 | liveAgentCount={agentPanelModel.liveCount} | ||
| 6422 | > | 6428 | > |
| 6423 | {rightPanelContent} | 6429 | {rightPanelContent} |
| 6424 | </RightPanelTabs> | 6430 | </RightPanelTabs> |
| ... | @@ -6447,6 +6453,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -6447,6 +6453,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 6447 | browserAvailable={isPreviewSupportedInRuntime()} | 6453 | browserAvailable={isPreviewSupportedInRuntime()} |
| 6448 | diffAvailable={isServerThread && isGitRepo} | 6454 | diffAvailable={isServerThread && isGitRepo} |
| 6449 | filesAvailable={activeProject !== null} | 6455 | filesAvailable={activeProject !== null} |
| 6456 | liveAgentCount={agentPanelModel.liveCount} | ||
| 6450 | > | 6457 | > |
| 6451 | {rightPanelContent} | 6458 | {rightPanelContent} |
| 6452 | </RightPanelTabs> | 6459 | </RightPanelTabs> |
apps/web/src/components/RightPanelTabs.tsx+20-1| ... | @@ -48,6 +48,8 @@ interface RightPanelTabsProps { | ... | @@ -48,6 +48,8 @@ interface RightPanelTabsProps { |
| 48 | browserAvailable: boolean; | 48 | browserAvailable: boolean; |
| 49 | diffAvailable: boolean; | 49 | diffAvailable: boolean; |
| 50 | filesAvailable: boolean; | 50 | filesAvailable: boolean; |
| 51 | /** Running + waiting subagents; badges the Agents card in the empty state. */ | ||
| 52 | liveAgentCount: number; | ||
| 51 | children: ReactNode; | 53 | children: ReactNode; |
| 52 | } | 54 | } |
| 53 | 55 | ||
| ... | @@ -96,6 +98,7 @@ function RightPanelEmptyState(props: { | ... | @@ -96,6 +98,7 @@ function RightPanelEmptyState(props: { |
| 96 | browserAvailable: boolean; | 98 | browserAvailable: boolean; |
| 97 | diffAvailable: boolean; | 99 | diffAvailable: boolean; |
| 98 | filesAvailable: boolean; | 100 | filesAvailable: boolean; |
| 101 | liveAgentCount: number; | ||
| 99 | }) { | 102 | }) { |
| 100 | const actions = [ | 103 | const actions = [ |
| 101 | { | 104 | { |
| ... | @@ -105,6 +108,7 @@ function RightPanelEmptyState(props: { | ... | @@ -105,6 +108,7 @@ function RightPanelEmptyState(props: { |
| 105 | available: props.browserAvailable, | 108 | available: props.browserAvailable, |
| 106 | disabledReason: SURFACE_DISABLED_REASONS.browser, | 109 | disabledReason: SURFACE_DISABLED_REASONS.browser, |
| 107 | onClick: props.onAddBrowser, | 110 | onClick: props.onAddBrowser, |
| 111 | badgeCount: 0, | ||
| 108 | }, | 112 | }, |
| 109 | { | 113 | { |
| 110 | label: "Terminal", | 114 | label: "Terminal", |
| ... | @@ -113,6 +117,7 @@ function RightPanelEmptyState(props: { | ... | @@ -113,6 +117,7 @@ function RightPanelEmptyState(props: { |
| 113 | available: true, | 117 | available: true, |
| 114 | disabledReason: null, | 118 | disabledReason: null, |
| 115 | onClick: props.onAddTerminal, | 119 | onClick: props.onAddTerminal, |
| 120 | badgeCount: 0, | ||
| 116 | }, | 121 | }, |
| 117 | { | 122 | { |
| 118 | label: "Files", | 123 | label: "Files", |
| ... | @@ -121,6 +126,7 @@ function RightPanelEmptyState(props: { | ... | @@ -121,6 +126,7 @@ function RightPanelEmptyState(props: { |
| 121 | available: props.filesAvailable, | 126 | available: props.filesAvailable, |
| 122 | disabledReason: SURFACE_DISABLED_REASONS.files, | 127 | disabledReason: SURFACE_DISABLED_REASONS.files, |
| 123 | onClick: props.onAddFiles, | 128 | onClick: props.onAddFiles, |
| 129 | badgeCount: 0, | ||
| 124 | }, | 130 | }, |
| 125 | { | 131 | { |
| 126 | label: "Diff", | 132 | label: "Diff", |
| ... | @@ -129,6 +135,7 @@ function RightPanelEmptyState(props: { | ... | @@ -129,6 +135,7 @@ function RightPanelEmptyState(props: { |
| 129 | available: props.diffAvailable, | 135 | available: props.diffAvailable, |
| 130 | disabledReason: SURFACE_DISABLED_REASONS.diff, | 136 | disabledReason: SURFACE_DISABLED_REASONS.diff, |
| 131 | onClick: props.onAddDiff, | 137 | onClick: props.onAddDiff, |
| 138 | badgeCount: 0, | ||
| 132 | }, | 139 | }, |
| 133 | { | 140 | { |
| 134 | label: "Agents", | 141 | label: "Agents", |
| ... | @@ -137,6 +144,7 @@ function RightPanelEmptyState(props: { | ... | @@ -137,6 +144,7 @@ function RightPanelEmptyState(props: { |
| 137 | available: true, | 144 | available: true, |
| 138 | disabledReason: null, | 145 | disabledReason: null, |
| 139 | onClick: props.onAddAgents, | 146 | onClick: props.onAddAgents, |
| 147 | badgeCount: props.liveAgentCount, | ||
| 140 | }, | 148 | }, |
| 141 | ] as const; | 149 | ] as const; |
| 142 | 150 | ||
| ... | @@ -154,7 +162,17 @@ function RightPanelEmptyState(props: { | ... | @@ -154,7 +162,17 @@ function RightPanelEmptyState(props: { |
| 154 | const Icon = action.icon; | 162 | const Icon = action.icon; |
| 155 | const content = ( | 163 | const content = ( |
| 156 | <> | 164 | <> |
| 157 | <Icon className="mb-3 size-5" /> | 165 | <span className="relative mb-3 inline-flex"> |
| 166 | <Icon className="size-5" /> | ||
| 167 | {action.badgeCount > 0 ? ( | ||
| 168 | <span | ||
| 169 | aria-hidden | ||
| 170 | className="absolute -top-1.5 -right-2 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-info px-1 text-[9px] font-semibold tabular-nums text-white" | ||
| 171 | > | ||
| 172 | {action.badgeCount} | ||
| 173 | </span> | ||
| 174 | ) : null} | ||
| 175 | </span> | ||
| 158 | <span className="text-sm font-medium">{action.label}</span> | 176 | <span className="text-sm font-medium">{action.label}</span> |
| 159 | <span className="mt-1 text-xs leading-relaxed text-muted-foreground"> | 177 | <span className="mt-1 text-xs leading-relaxed text-muted-foreground"> |
| 160 | {action.description} | 178 | {action.description} |
| ... | @@ -498,6 +516,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { | ... | @@ -498,6 +516,7 @@ export function RightPanelTabs(props: RightPanelTabsProps) { |
| 498 | browserAvailable={props.browserAvailable} | 516 | browserAvailable={props.browserAvailable} |
| 499 | diffAvailable={props.diffAvailable} | 517 | diffAvailable={props.diffAvailable} |
| 500 | filesAvailable={props.filesAvailable} | 518 | filesAvailable={props.filesAvailable} |
| 519 | liveAgentCount={props.liveAgentCount} | ||
| 501 | /> | 520 | /> |
| 502 | ) : ( | 521 | ) : ( |
| 503 | props.children | 522 | props.children |
apps/web/src/components/chat/PanelLayoutControls.tsx+21-2| ... | @@ -11,6 +11,8 @@ interface PanelLayoutControlsProps { | ... | @@ -11,6 +11,8 @@ interface PanelLayoutControlsProps { |
| 11 | rightPanelAvailable: boolean; | 11 | rightPanelAvailable: boolean; |
| 12 | rightPanelOpen: boolean; | 12 | rightPanelOpen: boolean; |
| 13 | rightPanelShortcutLabel: string | null; | 13 | rightPanelShortcutLabel: string | null; |
| 14 | /** Running + waiting subagents in this thread; badges the right panel toggle. */ | ||
| 15 | liveAgentCount: number; | ||
| 14 | onToggleTerminal: () => void; | 16 | onToggleTerminal: () => void; |
| 15 | onToggleRightPanel: () => void; | 17 | onToggleRightPanel: () => void; |
| 16 | } | 18 | } |
| ... | @@ -22,6 +24,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ | ... | @@ -22,6 +24,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 22 | rightPanelAvailable, | 24 | rightPanelAvailable, |
| 23 | rightPanelOpen, | 25 | rightPanelOpen, |
| 24 | rightPanelShortcutLabel, | 26 | rightPanelShortcutLabel, |
| 27 | liveAgentCount, | ||
| 25 | onToggleTerminal, | 28 | onToggleTerminal, |
| 26 | onToggleRightPanel, | 29 | onToggleRightPanel, |
| 27 | }: PanelLayoutControlsProps) { | 30 | }: PanelLayoutControlsProps) { |
| ... | @@ -59,18 +62,34 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ | ... | @@ -59,18 +62,34 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({ |
| 59 | className="shrink-0 [-webkit-app-region:no-drag]" | 62 | className="shrink-0 [-webkit-app-region:no-drag]" |
| 60 | pressed={rightPanelOpen} | 63 | pressed={rightPanelOpen} |
| 61 | onPressedChange={onToggleRightPanel} | 64 | onPressedChange={onToggleRightPanel} |
| 62 | aria-label="Toggle right panel" | 65 | aria-label={ |
| 66 | liveAgentCount > 0 | ||
| 67 | ? `Toggle right panel, ${liveAgentCount} ${liveAgentCount === 1 ? "agent" : "agents"} working` | ||
| 68 | : "Toggle right panel" | ||
| 69 | } | ||
| 63 | variant="ghost" | 70 | variant="ghost" |
| 64 | size="sm" | 71 | size="sm" |
| 65 | disabled={!rightPanelAvailable} | 72 | disabled={!rightPanelAvailable} |
| 66 | > | 73 | > |
| 67 | <PanelRightIcon className="size-3.5" /> | 74 | <PanelRightIcon className="size-3.5" /> |
| 75 | {liveAgentCount > 0 ? ( | ||
| 76 | <span | ||
| 77 | aria-hidden | ||
| 78 | className="absolute -top-1 -right-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-info px-1 text-[9px] font-semibold tabular-nums text-white" | ||
| 79 | > | ||
| 80 | {liveAgentCount} | ||
| 81 | </span> | ||
| 82 | ) : null} | ||
| 68 | </Toggle> | 83 | </Toggle> |
| 69 | } | 84 | } |
| 70 | /> | 85 | /> |
| 71 | <TooltipPopup side="bottom"> | 86 | <TooltipPopup side="bottom"> |
| 72 | {rightPanelAvailable | 87 | {rightPanelAvailable |
| 73 | ? `Toggle right panel${rightPanelShortcutLabel ? ` (${rightPanelShortcutLabel})` : ""}` | 88 | ? `Toggle right panel${rightPanelShortcutLabel ? ` (${rightPanelShortcutLabel})` : ""}${ |
| 89 | liveAgentCount > 0 | ||
| 90 | ? ` · ${liveAgentCount} ${liveAgentCount === 1 ? "agent" : "agents"} working` | ||
| 91 | : "" | ||
| 92 | }` | ||
| 74 | : "Right panel is unavailable"} | 93 | : "Right panel is unavailable"} |
| 75 | </TooltipPopup> | 94 | </TooltipPopup> |
| 76 | </Tooltip> | 95 | </Tooltip> |