diff --git a/__tests__/integration/components/TerminalOutput.test.tsx b/__tests__/integration/components/TerminalOutput.test.tsx
index 9f2edf6c..284246f1 100644
--- a/__tests__/integration/components/TerminalOutput.test.tsx
+++ b/__tests__/integration/components/TerminalOutput.test.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import { StyleSheet } from 'react-native'
-import { render } from '@testing-library/react-native'
+import { fireEvent, render } from '@testing-library/react-native'
import { TerminalOutput } from '@/components/terminal/TerminalOutput'
describe('TerminalOutput – rendering', () => {
@@ -153,3 +153,39 @@ describe('TerminalOutput – row testIDs', () => {
expect(queryAllByTestId('terminal-line-row')).toHaveLength(2)
})
})
+
+describe('TerminalOutput – resumed scrollback notice', () => {
+ it('does not render the notice without onViewResumedConversation', async () => {
+ const { queryByTestId } = await render(
+
+ )
+ expect(queryByTestId('terminal-resumed-scrollback-notice')).toBeNull()
+ })
+
+ it('renders the notice at the top of scrollback when the callback is set', async () => {
+ const onView = jest.fn()
+ const { getByTestId, getByText } = await render(
+
+ )
+ expect(getByTestId('terminal-resumed-scrollback-notice')).toBeTruthy()
+ expect(getByText("Earlier output isn't available for a resumed session.")).toBeTruthy()
+ expect(getByText('View the conversation history →')).toBeTruthy()
+ })
+
+ it('invokes onViewResumedConversation when the notice is pressed', async () => {
+ const onView = jest.fn()
+ const { getByTestId } = await render(
+
+ )
+ await fireEvent.press(getByTestId('terminal-resumed-scrollback-notice'))
+ expect(onView).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/__tests__/integration/components/TerminalView.test.tsx b/__tests__/integration/components/TerminalView.test.tsx
index eb3c0401..85bda87a 100644
--- a/__tests__/integration/components/TerminalView.test.tsx
+++ b/__tests__/integration/components/TerminalView.test.tsx
@@ -29,6 +29,12 @@ jest.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
}))
+const mockPush = jest.fn()
+jest.mock('expo-router', () => ({
+ useRouter: () => ({ push: mockPush, replace: jest.fn(), back: jest.fn() }),
+ useLocalSearchParams: () => ({}),
+}))
+
// ── feature mocks ────────────────────────────────────────────────────────────
const mockSendInputMutate = jest.fn()
const mockSendKeysMutate = jest.fn()
@@ -93,9 +99,9 @@ jest.mock('@/components/queue/PlanPreviewSheet', () => ({
// eslint-disable-next-line import/first
import { TerminalView } from '@/components/terminal/TerminalView'
-async function renderView() {
+async function renderView(props?: { resumedConversationId?: string }) {
return await render(
- ,
+ ,
{ wrapper: createWrapper() },
)
}
@@ -104,6 +110,7 @@ describe('TerminalView', () => {
beforeEach(() => {
mockSendInputMutate.mockClear()
mockSendKeysMutate.mockClear()
+ mockPush.mockClear()
})
it('renders terminal-line-row elements when lines are provided', async () => {
@@ -122,4 +129,17 @@ describe('TerminalView', () => {
await fireEvent.press(screen.getByTestId('chat-send-button'))
expect(mockSendInputMutate).toHaveBeenCalledWith('test-payload', expect.anything())
})
+
+ it('does not show the resumed scrollback notice for a fresh session', async () => {
+ await renderView()
+ expect(screen.queryByTestId('terminal-resumed-scrollback-notice')).toBeNull()
+ })
+
+ it('shows the resumed scrollback notice and navigates to the conversation', async () => {
+ await renderView({ resumedConversationId: 'conv-42' })
+ const notice = screen.getByTestId('terminal-resumed-scrollback-notice')
+ expect(notice).toBeTruthy()
+ await fireEvent.press(notice)
+ expect(mockPush).toHaveBeenCalledWith('/conversation/conv-42?server=srv1')
+ })
})
diff --git a/app/session/[id].tsx b/app/session/[id].tsx
index 4002c6ca..0267342f 100644
--- a/app/session/[id].tsx
+++ b/app/session/[id].tsx
@@ -1008,6 +1008,7 @@ export default function SessionDetailScreen() {
disabled={isWakingUp}
pendingPlan={planVisible ? pendingPlan : null}
onClosePlan={() => { setPlanVisible(false); setPendingPlan(null) }}
+ resumedConversationId={session.resumedFromConversationId}
/>
) : (
) => void
+ /**
+ * Resumed sessions start a fresh PTY — prior terminal bytes are gone.
+ * When set, show a scrollback-top disclosure linking to the durable conversation.
+ */
+ onViewResumedConversation?: () => void
}
-export function TerminalOutput({ lines, isStreaming: _isStreaming, userMessageTexts, onSendInput, onSendKeys, activeQuestion, onAnswer }: Props) {
+export function TerminalOutput({
+ lines,
+ isStreaming: _isStreaming,
+ userMessageTexts,
+ onSendInput,
+ onSendKeys,
+ activeQuestion,
+ onAnswer,
+ onViewResumedConversation,
+}: Props) {
const { t } = useTranslation('common')
+ const { t: tTerminal } = useTranslation('terminal')
const collapsedLines = useMemo(
() => collapseWrappedUserLines(lines, userMessageTexts),
[lines, userMessageTexts],
@@ -211,6 +226,22 @@ export function TerminalOutput({ lines, isStreaming: _isStreaming, userMessageTe
onAnswer?.(activeQuestion.toolUseId, { [q.question]: q.options[optionIndex].label })
}, [activeQuestion, onAnswer, onSendKeys])
+ const listHeader = useMemo(() => {
+ if (!onViewResumedConversation) return null
+ return (
+
+ {tTerminal('session.resumedEmptyScrollback')}
+ {tTerminal('session.resumedEmptyScrollbackLink')}
+
+ )
+ }, [onViewResumedConversation, tTerminal])
+
return (
void
+ /** Conversation that was resumed into this session — when set, disclose missing PTY scrollback. */
+ resumedConversationId?: string | null
}
export function TerminalView({
@@ -35,8 +39,10 @@ export function TerminalView({
disabled = false,
pendingPlan = null,
onClosePlan,
+ resumedConversationId = null,
}: Props) {
const { t } = useTranslation('terminal')
+ const router = useRouter()
const { lines, isStreaming, userMessageTexts, parseConfidence } = useTerminalStream(
serverId,
sessionId,
@@ -47,6 +53,11 @@ export function TerminalView({
const { sendInput, sendKeys, respondToQuestion } = useSessionActions(serverId, sessionId)
const { question: activeQuestion } = useActiveQuestion(serverId, sessionId)
+ const onViewResumedConversation = useCallback(() => {
+ if (!resumedConversationId) return
+ router.push(conversationHref(resumedConversationId, serverId) as never)
+ }, [resumedConversationId, router, serverId])
+
const onSend = (payload: string) => {
markSessionUsed(sessionId)
sendInput.mutate(payload, {
@@ -91,6 +102,7 @@ export function TerminalView({
onSendKeys={(keys) => sendKeys.mutate(keys)}
activeQuestion={activeQuestion}
onAnswer={(toolUseId, answers) => respondToQuestion.mutate({ toolUseId, answers })}
+ onViewResumedConversation={resumedConversationId ? onViewResumedConversation : undefined}
/>