Skip to content

Commit 3ec48e3

Browse files
committed
fix: address qa-swarm round-2 findings on the stale-conversation gate
- remount the notice when the option set changes so ActionSelector's highlighted index can't dereference past the list or fire a different option than the one shown - keep the gate covering the composer slot while reconnecting (handoff can leave pendingPermissions set; the connecting state shows instead of an answerable permission prompt) - derive the choice union from StaleConversationGateChoiceProperties - handleStaleNewSession is a useCallback like its siblings, with the undefined guard at the call site - story for the two-option permission-pending layout Generated-By: PostHog Code Task-Id: 1ec8e82e-b126-48b8-8fd4-54edcaf041d5
1 parent 92a819f commit 3ec48e3

2 files changed

Lines changed: 71 additions & 34 deletions

File tree

packages/ui/src/features/sessions/components/SessionView.tsx

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
type SessionService,
66
} from "@posthog/core/sessions/sessionService";
77
import { useService } from "@posthog/di/react";
8-
import { type AcpMessage, ANALYTICS_EVENTS } from "@posthog/shared";
8+
import {
9+
type AcpMessage,
10+
ANALYTICS_EVENTS,
11+
type StaleConversationGateChoiceProperties,
12+
} from "@posthog/shared";
913
import type { Task, TaskRunStatus } from "@posthog/shared/domain-types";
1014
import { showOfflineToast } from "@posthog/ui/features/connectivity/connectivityToast";
1115
import {
@@ -273,7 +277,7 @@ export function SessionView({
273277
const staleGate = useStaleConversationGate(sessionId, events);
274278

275279
const trackStaleGateChoice = useCallback(
276-
(choice: "compact" | "continue" | "new_session") =>
280+
(choice: StaleConversationGateChoiceProperties["choice"]) =>
277281
track(ANALYTICS_EVENTS.STALE_CONVERSATION_GATE_CHOICE, {
278282
choice,
279283
used_tokens: staleGate.usedTokens,
@@ -297,12 +301,9 @@ export function SessionView({
297301
staleGate.onContinue();
298302
}, [trackStaleGateChoice, staleGate.onContinue]);
299303

300-
const handleStaleNewSession = useMemo(() => {
301-
if (!onNewSession) return undefined;
302-
return () => {
303-
trackStaleGateChoice("new_session");
304-
onNewSession();
305-
};
304+
const handleStaleNewSession = useCallback(() => {
305+
trackStaleGateChoice("new_session");
306+
onNewSession?.();
306307
}, [trackStaleGateChoice, onNewSession]);
307308

308309
const [isDraggingFile, setIsDraggingFile] = useState(false);
@@ -606,37 +607,62 @@ export function SessionView({
606607
)}
607608
</Flex>
608609
</Flex>
609-
) : hideInput ? null : staleGate.active && isRunning ? (
610+
) : hideInput ? null : staleGate.active ? (
610611
// Replaces the composer (and any pending permission — answering
611612
// one also resumes the costly turn) until the user chooses.
612-
// Waits for isRunning so the choices can't fire into a session
613-
// that is still reconnecting.
614-
<Box className="min-h-0 shrink-0 overflow-y-auto">
615-
<Box
616-
className={compact ? "p-1" : "mx-auto px-2 pb-3"}
617-
style={
618-
compact
619-
? undefined
620-
: { maxWidth: CHAT_CONTENT_MAX_WIDTH }
621-
}
622-
>
623-
<StaleConversationCostNotice
624-
usedTokens={staleGate.usedTokens}
625-
lastActivityAt={staleGate.lastActivityAt}
626-
costUsd={staleGate.costUsd}
627-
onContinue={handleStaleContinue}
628-
// A queued /compact would land after the pending
629-
// permission resumes the costly turn — paying the
630-
// reload twice — so the option hides until then.
631-
onCompact={
632-
firstPendingPermission
613+
isRunning ? (
614+
<Box className="min-h-0 shrink-0 overflow-y-auto">
615+
<Box
616+
className={compact ? "p-1" : "mx-auto px-2 pb-3"}
617+
style={
618+
compact
633619
? undefined
634-
: handleStaleCompact
620+
: { maxWidth: CHAT_CONTENT_MAX_WIDTH }
635621
}
636-
onNewSession={handleStaleNewSession}
637-
/>
622+
>
623+
<StaleConversationCostNotice
624+
// Remount when the option set changes so the
625+
// selector's highlighted index can't dereference or
626+
// fire a different option than the one shown.
627+
key={
628+
firstPendingPermission
629+
? "no-compact"
630+
: "with-compact"
631+
}
632+
usedTokens={staleGate.usedTokens}
633+
lastActivityAt={staleGate.lastActivityAt}
634+
costUsd={staleGate.costUsd}
635+
onContinue={handleStaleContinue}
636+
// A queued /compact would land after the pending
637+
// permission resumes the costly turn — paying the
638+
// reload twice — so the option hides until then.
639+
onCompact={
640+
firstPendingPermission
641+
? undefined
642+
: handleStaleCompact
643+
}
644+
onNewSession={
645+
onNewSession ? handleStaleNewSession : undefined
646+
}
647+
/>
648+
</Box>
638649
</Box>
639-
</Box>
650+
) : (
651+
// While reconnecting the gate still covers the composer
652+
// slot: handoff can leave pendingPermissions set, and the
653+
// choices must not fire into a half-connected session.
654+
<Flex
655+
align="center"
656+
justify="center"
657+
gap="2"
658+
className="min-h-[66px]"
659+
>
660+
<Spinner size={28} className="animate-spin text-gray-9" />
661+
<Text color="gray" className="text-base">
662+
Connecting to agent...
663+
</Text>
664+
</Flex>
665+
)
640666
) : firstPendingPermission ? (
641667
// This box replaces the composer while a permission is pending, so it's an input
642668
// region: `shrink-0` keeps it from being compressed by the scroller above, and

packages/ui/src/features/sessions/components/StaleConversationCostNotice.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,14 @@ export const WithoutNewSessionOrCost: Story = {
7979
onCompact: () => {},
8080
},
8181
};
82+
83+
/** Compact is hidden while a permission is pending — two-option layout. */
84+
export const PermissionPending: Story = {
85+
args: {
86+
usedTokens: 481_000,
87+
lastActivityAt: TWO_HOURS_AGO,
88+
costUsd: 12.34,
89+
onContinue: () => {},
90+
onNewSession: () => {},
91+
},
92+
};

0 commit comments

Comments
 (0)