fix(web-shell): keep managed session actions when the host shows identity

The showManagedSessionHeader option gated the whole ChatContextHeader, so
turning it off also removed the session-actions menu and both panel toggles.
The home layout has no other surface carrying them, and App now always passes
a non-undefined navigationTarget, so the `!home` row is unreachable: archiving
a managed Session became a one-way door for host-shell layouts. Gate only the
identity payload (title, workspace icon, phase badge) and keep the action
cluster mounted.

The host composer footer was also told disabled/isRunning from a different
expression than the composer above it, so a restored pending prompt or a
blocked lifecycle action could leave an enabled send control that hit submit's
early return and silently did nothing. Hoist one composerDisabled predicate
and pass the same value to both.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw6hw90xj4
This commit is contained in:
jinjing.zzjandQwen-Coder committed 2026-11-07 22:46:42 +08:00
1 parent 080a6f1744
commit f670c1489d
2 files changed
+42 -30

No files matched your search

@@ -4451,6 +4451,11 @@ describe('ManagedSessionsPage', () => {
},
);
expect(container.textContent).not.toContain('Task s1');
// Hiding the identity must not unmount the header itself: its action
// cluster and panel toggles are the only surfaces that carry them.
expect(
container.querySelector('[data-testid="chat-context-header"]'),
).not.toBeNull();
});
it('keeps managed copy by default so the standalone page is unchanged', async () => {
@@ -1636,6 +1636,22 @@ function ManagedSessionsContent({
/>
);
}
// The host composer footer must not disagree with the composer it sits under,
// so both read this single predicate (ChatPane passes one expression too).
const composerDisabled =
!interactive ||
busy ||
Boolean(pending) ||
lifecycle.blocked(sessionId) ||
modelChange.blocked ||
compression.blocked ||
approvalChange.blocked ||
Boolean(
sessionId &&
(!summary?.capabilities.canSend ||
recovery.value?.nextTurnAllowed === false) &&
!userMessages.supported,
);
return (
<div
ref={pageRef}
@@ -1776,30 +1792,35 @@ function ManagedSessionsContent({
}
aria-label={t('managed.conversation')}
>
{summary && home && showManagedSessionHeader && (
{summary && home && (
<div className="shrink-0">
<ChatContextHeader
content={
<span className="flex min-w-0 items-center gap-2">
<span
className="truncate"
onDoubleClick={() => rename.open()}
>
{summary.title === summary.sessionId
? summary.sessionId.slice(0, 8)
: summary.title}
</span>
{showManagedSessionHeader && (
<span
className="truncate"
onDoubleClick={() => rename.open()}
>
{summary.title === summary.sessionId
? summary.sessionId.slice(0, 8)
: summary.title}
</span>
)}
{sessionActions(summary)}
</span>
}
hideWorkspaceIcon={!showManagedSessionHeader}
workspaceName={summary.workspace?.workspaceId}
location={
<Badge variant="secondary">
{t(`managed.phase.${summary.phase}`)}
{summary.lifecycleStatus &&
summary.lifecycleStatus !== 'active' &&
` · ${t(`managed.sessionState.${summary.lifecycleStatus}`)}`}
</Badge>
showManagedSessionHeader ? (
<Badge variant="secondary">
{t(`managed.phase.${summary.phase}`)}
{summary.lifecycleStatus &&
summary.lifecycleStatus !== 'active' &&
` · ${t(`managed.sessionState.${summary.lifecycleStatus}`)}`}
</Badge>
) : undefined
}
environmentOpen={taskPanelOpen}
environmentAvailable={Boolean(
@@ -2598,21 +2619,7 @@ function ManagedSessionsContent({
? () => void cancel()
: undefined
}
disabled={
!interactive ||
busy ||
Boolean(pending) ||
lifecycle.blocked(sessionId) ||
modelChange.blocked ||
compression.blocked ||
approvalChange.blocked ||
Boolean(
sessionId &&
(!summary?.capabilities.canSend ||
recovery.value?.nextTurnAllowed === false) &&
!userMessages.supported,
)
}
disabled={composerDisabled}
placeholderText={t(
active && userMessages.supported
? 'managed.queue.placeholder'