Skip to content

Commit 3cb8aba

Browse files
Merge pull request #144 from ai-action/feat/compact
2 parents bb3e908 + 3fa67fe commit 3cb8aba

12 files changed

Lines changed: 551 additions & 12 deletions

File tree

src/components/App/App.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const listSessions = vi.hoisted(() => vi.fn());
2929
const deleteSession = vi.hoisted(() => vi.fn());
3030
const deleteSessionIfEmpty = vi.hoisted(() => vi.fn());
3131
const appendMessage = vi.hoisted(() => vi.fn());
32+
const replaceMessages = vi.hoisted(() => vi.fn());
3233
const updateSessionModel = vi.hoisted(() => vi.fn());
3334
const saveConfig = vi.hoisted(() => vi.fn());
3435
const checkHealth = vi.hoisted(() => vi.fn());
@@ -62,6 +63,7 @@ vi.mock('@/utils', async () => ({
6263
deleteSessionIfEmpty,
6364
listSessions,
6465
loadSession,
66+
replaceMessages,
6567
updateSessionModel,
6668
},
6769
terminal: {
@@ -85,6 +87,9 @@ const capturedCallbacks = vi.hoisted(() => ({
8587
onMessagesChange: null as
8688
| ((messages: { role: string; content: string }[]) => void)
8789
| null,
90+
onMessagesReplace: null as
91+
| ((messages: { role: string; content: string }[]) => void)
92+
| null,
8893
}));
8994

9095
vi.mock('@/components/Header', () => ({
@@ -97,18 +102,21 @@ vi.mock('@/components/Chat', () => ({
97102
Chat: ({
98103
onCommand,
99104
onMessagesChange,
105+
onMessagesReplace,
100106
onModeChange,
101107
sessionId,
102108
}: {
103109
model: string;
104110
onCommand: (command: string) => void;
105111
onMessagesChange?: (messages: { role: string; content: string }[]) => void;
112+
onMessagesReplace?: (messages: { role: string; content: string }[]) => void;
106113
mode: string;
107114
onModeChange: (mode: string) => void;
108115
sessionId: string;
109116
}) => {
110117
capturedCallbacks.onCommand = onCommand;
111118
capturedCallbacks.onMessagesChange = onMessagesChange ?? null;
119+
capturedCallbacks.onMessagesReplace = onMessagesReplace ?? null;
112120
capturedCallbacks.onModeChange = onModeChange;
113121
return <Text>{`> session:${sessionId}`}</Text>;
114122
},
@@ -266,6 +274,7 @@ describe('App', () => {
266274
capturedCallbacks.onDeleteSession = null;
267275
capturedCallbacks.onNewSession = null;
268276
capturedCallbacks.onMessagesChange = null;
277+
capturedCallbacks.onMessagesReplace = null;
269278

270279
resetSystemMessage.mockClear();
271280
clearScreen.mockClear();
@@ -278,6 +287,7 @@ describe('App', () => {
278287
deleteSession.mockReset();
279288
deleteSessionIfEmpty.mockReset();
280289
appendMessage.mockReset();
290+
replaceMessages.mockReset();
281291
updateSessionModel.mockReset();
282292
saveConfig.mockReset();
283293
checkHealth.mockReset();
@@ -329,6 +339,17 @@ describe('App', () => {
329339
directory: process.cwd(),
330340
}));
331341

342+
replaceMessages.mockImplementation(
343+
(_sessionId, _messages, model: string) => ({
344+
id: 'session-0',
345+
createdAt: '2026-05-11T00:00:00.000Z',
346+
updatedAt: '2026-05-11T00:00:01.000Z',
347+
title: 'Session 0',
348+
model,
349+
directory: process.cwd(),
350+
}),
351+
);
352+
332353
updateSessionModel.mockImplementation(
333354
(sessionId: string, model: string) => ({
334355
id: sessionId,
@@ -731,6 +752,27 @@ describe('App', () => {
731752
expect(appendMessage).not.toHaveBeenCalled();
732753
});
733754

755+
it('replaces persisted session messages when chat compacts history', async () => {
756+
await renderApp();
757+
758+
capturedCallbacks.onMessagesReplace?.([
759+
{ role: 'system', content: 'Compacted conversation context' },
760+
{ role: 'user', content: TURN_ABORTED_MESSAGE },
761+
{ role: 'assistant', content: 'latest reply' },
762+
]);
763+
764+
await time.tick();
765+
766+
expect(replaceMessages).toHaveBeenCalledWith(
767+
'session-0',
768+
[
769+
{ role: 'system', content: 'Compacted conversation context' },
770+
{ role: 'assistant', content: 'latest reply' },
771+
],
772+
'gemma4',
773+
);
774+
});
775+
734776
it('updates the active session model when the manager saves one', async () => {
735777
const { rerender } = await renderApp();
736778
capturedCallbacks.onCommand?.('/model');

src/components/App/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export function App({ sessionId, initialScreen }: Props) {
4242
handleOpenSession,
4343
handleDeleteSession,
4444
handleMessagesChange,
45+
handleMessagesReplace,
4546
} = useSessionManager({
4647
sessionId,
4748
model: appConfig.model ?? '',
@@ -229,6 +230,7 @@ export function App({ sessionId, initialScreen }: Props) {
229230
model={appConfig.model}
230231
onCommand={handleChatCommand}
231232
onMessagesChange={handleMessagesChange}
233+
onMessagesReplace={handleMessagesReplace}
232234
mode={mode}
233235
onModeChange={setMode}
234236
sessionId={activeSession.metadata.id}

src/components/App/hooks/useSessionManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,31 @@ export function useSessionManager({
114114
});
115115
}, []);
116116

117+
const handleMessagesReplace = useCallback((messages: ollama.Message[]) => {
118+
setSession((current) => {
119+
const persistedMessages = messages.filter(
120+
({ content }) => content !== TURN_ABORTED_MESSAGE,
121+
);
122+
const metadata = session.replaceMessages(
123+
current.metadata.id,
124+
persistedMessages,
125+
modelRef.current,
126+
);
127+
128+
return {
129+
metadata,
130+
messages: persistedMessages,
131+
};
132+
});
133+
}, []);
134+
117135
return {
118136
activeSession,
119137
setSession,
120138
handleCreateSession,
121139
handleOpenSession,
122140
handleDeleteSession,
123141
handleMessagesChange,
142+
handleMessagesReplace,
124143
};
125144
}

0 commit comments

Comments
 (0)