mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 08:03:33 +00:00
fix(mobile): scope the ask dismissal to the provider session, not the tab
A restart, /clear, or resume swaps the provider session inside one tab. The next session's first question is often byte-identical, so a tab-keyed dismissal hid the live card and left the turn blocked with nothing to act on.
This commit is contained in:
@@ -14,8 +14,9 @@ export function useMobileNativeChatAskDismiss(args: {
|
||||
* `ask`: reading the gated prompt as the detected one is the resurfacing bug
|
||||
* this hook exists to close. */
|
||||
detectedAsk: AskPrompt | null
|
||||
/** Dismissals are scoped to the tab that showed the card, so one tab's
|
||||
* dismissal can't hide an identical question on another tab. */
|
||||
/** Dismissals are scoped to the tab *and* provider session that showed the
|
||||
* card, so neither another tab nor a restarted session in the same tab can
|
||||
* have an identical question hidden by an answer that was never about it. */
|
||||
scopeKey: string | null
|
||||
/** True while the chat surface can actually observe the prompt. A null ask it
|
||||
* cannot see — off-chat, or before a re-subscribed transcript lands — proves
|
||||
|
||||
@@ -436,8 +436,9 @@ describe('useMobileNativeChatController ask dismissal across a transcript reload
|
||||
const PROMPT = { questions: [{ question: 'Which path?', multiSelect: false, options: [] }] }
|
||||
|
||||
const chatTab = { type: 'terminal', id: 'tab-1', launchAgent: 'claude' }
|
||||
/** Mutable so a test can move the user to another tab mid-render. */
|
||||
const activeTab = { id: 'tab-1' }
|
||||
/** Mutable so a test can move the user to another tab — or restart the agent
|
||||
* into a new provider session on the same tab — mid-render. */
|
||||
const activeTab = { id: 'tab-1', sessionId: 'session-1' }
|
||||
|
||||
function Harness(): null {
|
||||
controller = useMobileNativeChatController({
|
||||
@@ -445,7 +446,11 @@ describe('useMobileNativeChatController ask dismissal across a transcript reload
|
||||
connState: 'connected',
|
||||
hostId: 'h',
|
||||
worktreeId: 'w',
|
||||
activeSessionTab: { ...chatTab, id: activeTab.id } as never,
|
||||
activeSessionTab: {
|
||||
...chatTab,
|
||||
id: activeTab.id,
|
||||
agentStatus: { agentType: 'claude', providerSession: { id: activeTab.sessionId } }
|
||||
} as never,
|
||||
activeSessionTabId: activeTab.id,
|
||||
activeHandleRef: { current: 'term-1' },
|
||||
deviceTokenRef: { current: null },
|
||||
@@ -507,6 +512,7 @@ describe('useMobileNativeChatController ask dismissal across a transcript reload
|
||||
setTranscript('ready', 0)
|
||||
viewMode.isTabChatView = () => true
|
||||
activeTab.id = 'tab-1'
|
||||
activeTab.sessionId = 'session-1'
|
||||
})
|
||||
|
||||
it('scopes the dismissal to the tab it was taken on', () => {
|
||||
@@ -521,6 +527,29 @@ describe('useMobileNativeChatController ask dismissal across a transcript reload
|
||||
expect(controller?.nativeChatAsk).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows a restarted session’s identical first question on the same tab', () => {
|
||||
// A restart, `/clear`, or resume swaps the provider session inside one tab,
|
||||
// and the next session's first question is often byte-identical — same repo,
|
||||
// same prompt, same template. Keyed by tab alone the old answer hides it, so
|
||||
// the live card never renders and the turn sits blocked with nothing to act on.
|
||||
act(() => controller?.dismissNativeChatAsk())
|
||||
expect(controller?.nativeChatAsk).toBeNull()
|
||||
|
||||
// The restart re-subscribes the transcript, so the read is in flight for a beat.
|
||||
activeTab.sessionId = 'session-2'
|
||||
setTranscript('loading')
|
||||
promptsState.ask = null
|
||||
promptsState.detectedAsk = null
|
||||
step()
|
||||
|
||||
setTranscript('ready')
|
||||
promptsState.ask = PROMPT
|
||||
promptsState.detectedAsk = PROMPT
|
||||
step()
|
||||
|
||||
expect(controller?.nativeChatAsk).not.toBeNull()
|
||||
})
|
||||
|
||||
it('retires the dismissal only on the ungated prompt, not the gated one', () => {
|
||||
// The paused gate hides the card whenever the agent is not waiting, but a
|
||||
// hidden card is no evidence the sticky status prompt cleared. Feeding the
|
||||
|
||||
@@ -206,6 +206,13 @@ export function useMobileNativeChatController(args: {
|
||||
(nativeChatSession.status === 'error' && nativeChatSession.messages.length > 0)
|
||||
const nativeChatAskObservable =
|
||||
showNativeChat && (nativeChatDetectedAsk != null || nativeChatTranscriptSettled)
|
||||
// Scope the dismissal to the transcript the card was answered on, not the tab
|
||||
// alone: a restart, `/clear`, or resume swaps the provider session inside one
|
||||
// tab, and the new session's first question is often byte-identical (same repo,
|
||||
// same prompt, same template). Keyed by tab, the old answer read as "already
|
||||
// dismissed" and the live card never rendered. Same `\0` shape as
|
||||
// `streamIdentity` above, which is the identity the transcript itself tracks.
|
||||
const nativeChatAskScopeKey = `${activeSessionTabId ?? ''}\0${activeChatSessionId ?? ''}`
|
||||
const {
|
||||
askKey: nativeChatAskKey,
|
||||
showAsk: showNativeChatAsk,
|
||||
@@ -213,7 +220,7 @@ export function useMobileNativeChatController(args: {
|
||||
} = useMobileNativeChatAskDismiss({
|
||||
ask: nativeChatAskPrompt,
|
||||
detectedAsk: nativeChatDetectedAsk,
|
||||
scopeKey: activeSessionTabId,
|
||||
scopeKey: nativeChatAskScopeKey,
|
||||
observing: nativeChatAskObservable
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user