Skip to content
This repository was archived by the owner on Jan 30, 2026. It is now read-only.

Commit efe7a3c

Browse files
authored
fix: search for actual prev/next visible message in chain (#124)
The previous fix only checked if the immediately adjacent message was hidden. This caused gaps in chains when hidden messages (like lesson inclusions) appeared between visible messages. Now searches through the log to find the actual prev/next visible message, properly connecting chains across hidden messages.
1 parent 36d90cf commit efe7a3c

File tree

4 files changed

+66
-39
lines changed

4 files changed

+66
-39
lines changed

src/components/ChatMessage.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,12 @@ import { customRenderer, type CustomRenderer } from '@/utils/markdownRenderer';
1111

1212
interface Props {
1313
message$: Observable<Message | StreamingMessage>;
14-
previousMessage$?: Observable<Message | undefined>;
15-
nextMessage$?: Observable<Message | undefined>;
14+
log$: Observable<(Message | StreamingMessage)[]>;
15+
currentIndex: number;
1616
conversationId: string;
1717
}
1818

19-
export const ChatMessage: FC<Props> = ({
20-
message$,
21-
previousMessage$,
22-
nextMessage$,
23-
conversationId,
24-
}) => {
19+
export const ChatMessage: FC<Props> = ({ message$, log$, currentIndex, conversationId }) => {
2520
const { connectionConfig } = useApi();
2621
const { settings } = useSettings();
2722

@@ -208,7 +203,7 @@ export const ChatMessage: FC<Props> = ({
208203
);
209204
});
210205

211-
const chainType$ = useMessageChainType(message$, previousMessage$, nextMessage$);
206+
const chainType$ = useMessageChainType(message$, log$, currentIndex);
212207
const messageClasses$ = useObservable(
213208
() => `
214209
${

src/components/ConversationContent.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,12 @@ export const ConversationContent: FC<Props> = ({ conversationId, isReadOnly }) =
185185
return <div key={`${index}-${message?.timestamp}`} />;
186186
}
187187

188-
// Get the previous and next messages for spacing context
189-
const previousMessage$ = index > 0 ? conversation$.data.log[index - 1] : undefined;
190-
const nextMessage$ = conversation$.data.log[index + 1];
191-
192188
return (
193189
<ChatMessage
194190
key={`${index}-${msg$.timestamp.get()}`}
195191
message$={msg$}
196-
previousMessage$={previousMessage$}
197-
nextMessage$={nextMessage$}
192+
log$={conversation$.data.log}
193+
currentIndex={index}
198194
conversationId={conversationId}
199195
/>
200196
);

src/components/__tests__/ChatMessage.test.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,36 +25,61 @@ describe('ChatMessage', () => {
2525
};
2626

2727
it('renders user message', () => {
28-
const message$ = observable<Message>({
28+
const message: Message = {
2929
role: 'user',
3030
content: 'Hello!',
3131
timestamp: new Date().toISOString(),
32-
});
32+
};
33+
const message$ = observable<Message>(message);
34+
const log$ = observable<Message[]>([message]);
3335

34-
renderWithProviders(<ChatMessage message$={message$} conversationId={testConversationId} />);
36+
renderWithProviders(
37+
<ChatMessage
38+
message$={message$}
39+
log$={log$}
40+
currentIndex={0}
41+
conversationId={testConversationId}
42+
/>
43+
);
3544
expect(screen.getByText('Hello!')).toBeInTheDocument();
3645
});
3746

3847
it('renders assistant message', () => {
39-
const message$ = observable<Message>({
48+
const message: Message = {
4049
role: 'assistant',
4150
content: 'Hi there!',
4251
timestamp: new Date().toISOString(),
43-
});
52+
};
53+
const message$ = observable<Message>(message);
54+
const log$ = observable<Message[]>([message]);
4455

45-
renderWithProviders(<ChatMessage message$={message$} conversationId={testConversationId} />);
56+
renderWithProviders(
57+
<ChatMessage
58+
message$={message$}
59+
log$={log$}
60+
currentIndex={0}
61+
conversationId={testConversationId}
62+
/>
63+
);
4664
expect(screen.getByText('Hi there!')).toBeInTheDocument();
4765
});
4866

4967
it('renders system message with monospace font', () => {
50-
const message$ = observable<Message>({
68+
const message: Message = {
5169
role: 'system',
5270
content: 'System message',
5371
timestamp: new Date().toISOString(),
54-
});
72+
};
73+
const message$ = observable<Message>(message);
74+
const log$ = observable<Message[]>([message]);
5575

5676
const { container } = renderWithProviders(
57-
<ChatMessage message$={message$} conversationId={testConversationId} />
77+
<ChatMessage
78+
message$={message$}
79+
log$={log$}
80+
currentIndex={0}
81+
conversationId={testConversationId}
82+
/>
5883
);
5984
const messageElement = container.querySelector('.font-mono');
6085
expect(messageElement).toBeInTheDocument();

src/utils/messageUtils.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,48 @@
11
import type { Message } from '@/types/conversation';
2-
import { type Observable } from '@legendapp/state';
2+
import type { Observable } from '@legendapp/state';
33
import { useObservable } from '@legendapp/state/react';
44

55
export const isNonUserMessage = (role?: string) => role === 'assistant' || role === 'system';
66

7-
// Helper to check if a message should be considered for chain calculations
8-
// Hidden messages are treated as non-existent for chain purposes
7+
// Helper to check if a message should be visible for chain calculations
98
const isVisibleForChain = (message: Message | undefined): boolean => {
109
if (!message) return false;
11-
// Messages with hide=true should not affect chain calculations
1210
if (message.hide) return false;
1311
return true;
1412
};
1513

14+
// Find the previous visible message in the log
15+
const findPrevVisibleMessage = (log: Message[], currentIndex: number): Message | undefined => {
16+
for (let i = currentIndex - 1; i >= 0; i--) {
17+
if (isVisibleForChain(log[i])) return log[i];
18+
}
19+
return undefined;
20+
};
21+
22+
// Find the next visible message in the log
23+
const findNextVisibleMessage = (log: Message[], currentIndex: number): Message | undefined => {
24+
for (let i = currentIndex + 1; i < log.length; i++) {
25+
if (isVisibleForChain(log[i])) return log[i];
26+
}
27+
return undefined;
28+
};
29+
1630
export const useMessageChainType = (
1731
message$: Observable<Message>,
18-
previousMessage$: Observable<Message | undefined> | undefined,
19-
nextMessage$: Observable<Message | undefined> | undefined
32+
log$: Observable<Message[]>,
33+
currentIndex: number
2034
) => {
2135
const messageChainType$ = useObservable(() => {
2236
try {
2337
const message = message$.get();
2438
if (!message) return 'standalone';
2539

26-
const previousMessage = previousMessage$?.get();
27-
const nextMessage = nextMessage$?.get();
28-
29-
// Treat hidden messages as non-existent for chain calculations
30-
const prevVisible = isVisibleForChain(previousMessage);
31-
const nextVisible = isVisibleForChain(nextMessage);
40+
const log = log$.get() || [];
41+
const prevVisibleMessage = findPrevVisibleMessage(log, currentIndex);
42+
const nextVisibleMessage = findNextVisibleMessage(log, currentIndex);
3243

33-
const isChainStart = !prevVisible || previousMessage?.role === 'user';
34-
const isChainEnd = !nextVisible || nextMessage?.role === 'user';
44+
const isChainStart = !prevVisibleMessage || prevVisibleMessage.role === 'user';
45+
const isChainEnd = !nextVisibleMessage || nextVisibleMessage.role === 'user';
3546
const isPartOfChain = isNonUserMessage(message.role);
3647

3748
if (!isPartOfChain) return 'standalone';
@@ -43,6 +54,6 @@ export const useMessageChainType = (
4354
console.warn('Error calculating message chain type:', error);
4455
return 'standalone';
4556
}
46-
}, [message$, previousMessage$, nextMessage$]);
57+
}, [message$, log$, currentIndex]);
4758
return messageChainType$;
4859
};

0 commit comments

Comments
 (0)