Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ui/e2e/fixtures/streaming-lifecycle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><meta charset="utf-8" /><title>Streaming lifecycle regression</title></head><body><div id="root"></div><script type="module" src="/e2e/fixtures/streaming-lifecycle.jsx"></script></body></html>
107 changes: 107 additions & 0 deletions ui/e2e/fixtures/streaming-lifecycle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { FindShortcutProvider } from '../../src/contexts/FindShortcutContext';
import MessagesPane from '../../src/components/chat-v2/MessagesPaneV2';
import SubagentModal from '../../src/components/chat-v2/SubagentDetailModal';
import SubagentFlow from '../../src/components/chat-v2/SubagentDetailMessageFlow';
import { normalizedToChatMessages } from '../../src/components/chat/hooks/useChatMessages';
import { useSubagentMessages } from '../../src/components/chat-v2/useSubagentMessages';
import { useChatSessionState } from '../../src/components/chat/hooks/useChatSessionState';
import { useSessionStore } from '../../src/stores/useSessionStore';
import '../../src/index.css';

const params = new URLSearchParams(location.search);
const project = { name: 'fixture', path: '/fixture' };
const sessions = [{ id: 'a', isReadOnly: true }, { id: 'b', isReadOnly: true }];
const noop = () => {};
const diff = () => [];
const normalized = (sid, index) => ({
id: `${sid}-${index}`, sessionId: sid, timestamp: '2026-09-05T00:00:00Z',
provider: 'pilotdeck', kind: 'text', role: index % 2 ? 'assistant' : 'user',
content: `Session ${sid} message ${index}.\n\nA paragraph for reading.`,
});
const count = Number(params.get('count') || 240);
const data = Object.fromEntries(sessions.map((session) => [
session.id, Array.from({ length: count }, (_, i) => normalized(session.id, i)),
]));
const mockStore = {
setActiveSession: noop,
getMessages: (id) => data[id] || [],
has: () => true,
isStale: () => false,
fetchFromServer: async (id) => ({
status: 'idle', hasMore: false, total: data[id].length, serverMessages: data[id],
}),
};

function SessionFixture() {
const [session, setSession] = useState(sessions[0]);
const [auto, setAuto] = useState(params.get('auto') !== 'false');
const [, tick] = useState(0);
const pending = useRef(null);
const chat = useChatSessionState({
selectedProject: project, selectedSession: session, ws: null, sendMessage: noop,
autoScrollToBottom: auto, resetStreamingState: noop, pendingViewSessionRef: pending, sessionStore: mockStore,
});
window.streamLifecycle = {
chat, setSession: (index) => setSession(sessions[index]), setAuto,
append: (n = 1) => {
data[session.id] = [...data[session.id], ...Array.from({ length: n }, (_, i) => normalized(session.id, data[session.id].length + i))];
tick((value) => value + 1);
},
};
return <FindShortcutProvider activeScope="chat">
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<MessagesPane {...chat} provider="pilotdeck" selectedProject={project} selectedSession={session}
setInput={noop} showThinking showReturnToLatest={chat.canReturnToLatest}
onPauseScroll={chat.pauseScrollFollowing} onResumeScroll={chat.scrollToBottom} />
</div>
</FindShortcutProvider>;
}

function ChildFixture({ direct = false }) {
const store = useSessionStore();
const [status, setStatus] = useState('running');
useEffect(() => store.setActiveSession('s'), [store]);
const detail = useSubagentMessages(direct ? null : 's', direct ? null : 'child', undefined, store, status);
window.streamLifecycle = {
store, detail, status,
think: (text) => store.updateSubagentDetailThinking('s', 'child', text, 'pilotdeck'),
text: (text) => store.updateSubagentDetailStreaming('s', 'child', text, 'pilotdeck'),
finish: () => {
store.finalizeSubagentDetailThinking('s', 'child');
store.finalizeSubagentDetailStreaming('s', 'child');
setStatus('completed');
},
};
return <FindShortcutProvider activeScope="chat">
{direct ? <div style={{ height: 400, display: 'flex', flexDirection: 'column' }}>
<SubagentFlow messages={normalizedToChatMessages(store.getSubagentDetailMessages('s', 'child'))}
provider="pilotdeck" selectedProject={null} createDiff={diff} isRunning={status === 'running'} showThinking />
</div> : <SubagentModal subagentId="child" messages={detail.messages}
isLoading={detail.isLoading} error={detail.error} provider="pilotdeck" selectedProject={null} createDiff={diff}
showThinking isRunning={status === 'running'} onClose={noop} />}
</FindShortcutProvider>;
}

function TranscriptFixture() {
const [state, set] = useState({ messages: [], working: true, activities: [], showThinking: true });
const ref = useRef(null);
window.streamLifecycle = { ...state, set: (update) => set((value) => ({ ...value, ...update })) };
return <FindShortcutProvider activeScope="chat">
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<MessagesPane scrollContainerRef={ref} chatMessages={state.messages} visibleMessages={state.messages}
activityMessages={state.activities} visibleMessageCount={state.messages.length} totalMessages={state.messages.length}
isLoadingSessionMessages={false} isLoadingMoreMessages={false} hasMoreMessages={false} allMessagesLoaded
isLoadingAllMessages={false} loadAllMessages={noop} loadEarlierMessages={noop} provider="pilotdeck"
selectedProject={null} selectedSession={null} createDiff={diff} showThinking={state.showThinking} inlineThinking
isAssistantWorking={state.working} sessionRuntimeState={state.working ? 'running' : 'inactive'} activeRunId="run"
setInput={noop} />
</div>
</FindShortcutProvider>;
}

createRoot(document.getElementById('root')).render(
params.has('child-direct') ? <ChildFixture direct /> : params.has('child') ? <ChildFixture />
: params.has('transcript') ? <TranscriptFixture /> : <SessionFixture />,
);
2 changes: 2 additions & 0 deletions ui/e2e/fixtures/streaming-scroll.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><meta charset="utf-8" /><title>Streaming scroll regression</title></head><body><div id="root"></div><script type="module" src="/e2e/fixtures/streaming-scroll.jsx"></script></body></html>
73 changes: 73 additions & 0 deletions ui/e2e/fixtures/streaming-scroll.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { FindShortcutProvider } from '../../src/contexts/FindShortcutContext';
import MessagesPane from '../../src/components/chat-v2/MessagesPaneV2';
import { useScrollFollow } from '../../src/components/chat/hooks/useScrollFollow';
import { normalizedToChatMessages } from '../../src/components/chat/hooks/useChatMessages';
import { useSessionStore } from '../../src/stores/useSessionStore';
import '../../src/index.css';

const sid = 'scroll-fixture';
const timestamp = '2026-09-05T00:00:00.000Z';
const base = { sessionId: sid, provider: 'pilotdeck', timestamp, runId: 'live' };
function Fixture() {
const store = useSessionStore();
const [working, setWorking] = useState(false);
const [inline, setInline] = useState(true);
const [mode, setMode] = useState('agent');
const [olderMessages, setOlderMessages] = useState([]);
const ref = useRef(null);
const messages = [...olderMessages, ...normalizedToChatMessages(store.getMessages(sid))];
const follow = useScrollFollow({ containerRef: ref, enabled: true, scopeKey: sid, contentKey: messages.length > 0, contentSelector: '[data-chat-scroll-content]' });
useEffect(() => {
store.setActiveSession(sid);
store.appendRealtimeBatch(sid, Array.from({ length: Number(new URLSearchParams(location.search).get('history') ?? 24) }, (_, index) => [
{ ...base, id: `user-${index}`, kind: 'text', role: 'user', content: `Question ${index}` },
{ ...base, id: `answer-${index}`, kind: 'text', role: 'assistant', content: `Answer ${index}: This is a historical response with enough text to read while a new response streams.\n\nSecond paragraph with a searchable needle ${index}.` },
]).flat());
}, [store]);
window.streamFixture = {
prepend(count) {
setOlderMessages((previous) => [
...Array.from({ length: count }, (_, index) => ({
id: `older-${previous.length + index}`, type: index % 2 ? 'assistant' : 'user',
content: `Earlier message ${previous.length + index}: additional history.`, timestamp,
})), ...previous,
]);
},
think(text) {
setWorking(true);
store.updateStreamingThinking(sid, text, 'pilotdeck', 'live');
},
tool(id = 'fetch-1') {
store.finalizeStreamingThinking(sid, 'live');
store.appendRealtime(sid, { ...base, id, kind: 'tool_use', toolId: id, toolName: 'web_fetch', toolInput: { url: `https://example.com/${id}` } });
setWorking(true);
},
finishTool(id = 'fetch-1', content = '') {
store.appendRealtime(sid, { ...base, id: `${id}-result`, kind: 'tool_result', toolId: id, content, isError: false });
},
text(text) { setWorking(true); store.finalizeStreamingThinking(sid, 'live'); store.updateStreaming(sid, text, 'pilotdeck', 'live'); },
complete() { store.finalizeStreamingThinking(sid, 'live'); store.finalizeStreaming(sid, 'live'); setWorking(false); },
setInline, setMode,
};
const diff = useMemo(() => () => [], []);
return <FindShortcutProvider activeScope="chat"><div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<MessagesPane
scrollContainerRef={ref}
showReturnToLatest={follow.canReturnToLatest}
onResumeScroll={follow.scrollToBottom}
onPauseScroll={() => follow.setPaused(true)}
chatMessages={messages} visibleMessages={messages} visibleMessageCount={100}
isLoadingSessionMessages={false} isLoadingMoreMessages={false}
hasMoreMessages={false} totalMessages={messages.length}
loadEarlierMessages={() => {}} loadAllMessages={() => {}}
allMessagesLoaded={false} isLoadingAllMessages={false}
provider="pilotdeck" selectedProject={null} selectedSession={null}
createDiff={diff} showThinking inlineThinking={inline} setInput={() => {}}
isAssistantWorking={working} sessionRuntimeState={working ? 'running' : 'inactive'}
activeRunId="live" runMode={mode} planModeActive={mode === 'plan'}
/>
</div></FindShortcutProvider>;
}
createRoot(document.getElementById('root')).render(<Fixture />);
Loading
Loading