mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-08 20:58:45 +02:00
feat: allow branching from both user and assistant messages
- Relax branchSessionRecord validation to accept user or assistant roles - Show "Branch from here" hover button on all conversation messages - Contextual tooltip: "starting from this message" vs "continuing from this response" - Add test for branching from assistant message - Replace non-user rejection test with non-conversation rejection test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -380,11 +380,13 @@ export function ChatPane({
|
|||||||
{phaseLabel}
|
{phaseLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isUser && onBranchFromMessage && (
|
{onBranchFromMessage && (
|
||||||
<button
|
<button
|
||||||
className="ml-auto flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-[var(--color-text-muted)] opacity-0 transition-all duration-150 hover:bg-[var(--color-surface-2)] hover:text-[var(--color-accent)] group-hover:opacity-100"
|
className="ml-auto flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] text-[var(--color-text-muted)] opacity-0 transition-all duration-150 hover:bg-[var(--color-surface-2)] hover:text-[var(--color-accent)] group-hover:opacity-100"
|
||||||
onClick={() => onBranchFromMessage(message.id)}
|
onClick={() => onBranchFromMessage(message.id)}
|
||||||
title="Branch from here — create a new session starting from this message"
|
title={isUser
|
||||||
|
? 'Branch from here — create a new session starting from this message'
|
||||||
|
: 'Branch from here — create a new session continuing from this response'}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<GitBranch className="size-3" />
|
<GitBranch className="size-3" />
|
||||||
|
|||||||
@@ -237,8 +237,8 @@ export function branchSessionRecord(
|
|||||||
throw new Error(`Message ${messageId} not found in session ${session.id}.`);
|
throw new Error(`Message ${messageId} not found in session ${session.id}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceMessage.role !== 'user') {
|
if (sourceMessage.role !== 'user' && sourceMessage.role !== 'assistant') {
|
||||||
throw new Error('Only user messages can be used as a branch point.');
|
throw new Error('Only user or assistant messages can be used as a branch point.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchedMessages = session.messages.slice(0, sourceMessageIndex + 1).map(cloneChatMessageRecord);
|
const branchedMessages = session.messages.slice(0, sourceMessageIndex + 1).map(cloneChatMessageRecord);
|
||||||
|
|||||||
@@ -279,14 +279,14 @@ describe('session library helpers', () => {
|
|||||||
expect(sourceSession.messages[1]?.pending).toBe(true);
|
expect(sourceSession.messages[1]?.pending).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rejects branching from non-user messages', () => {
|
test('rejects branching from non-conversation messages', () => {
|
||||||
const sourceSession = createSession({
|
const sourceSession = createSession({
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
id: 'msg-1',
|
id: 'msg-1',
|
||||||
role: 'assistant',
|
role: 'system' as 'user',
|
||||||
authorName: 'Reviewer',
|
authorName: 'System',
|
||||||
content: 'I can help with that.',
|
content: 'System prompt.',
|
||||||
createdAt: '2026-03-23T00:00:00.000Z',
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -299,7 +299,57 @@ describe('session library helpers', () => {
|
|||||||
'session-branch',
|
'session-branch',
|
||||||
'msg-1',
|
'msg-1',
|
||||||
'2026-03-23T00:04:00.000Z',
|
'2026-03-23T00:04:00.000Z',
|
||||||
)).toThrow('Only user messages can be used as a branch point.');
|
)).toThrow('Only user or assistant messages can be used as a branch point.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('branches from an assistant message and retains transcript up to that response', () => {
|
||||||
|
const sourceSession = createSession({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 'msg-1',
|
||||||
|
role: 'user',
|
||||||
|
authorName: 'You',
|
||||||
|
content: 'Investigate the refresh bug.',
|
||||||
|
createdAt: '2026-03-23T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-2',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'Reviewer',
|
||||||
|
content: 'I found two likely causes.',
|
||||||
|
createdAt: '2026-03-23T00:01:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-3',
|
||||||
|
role: 'user',
|
||||||
|
authorName: 'You',
|
||||||
|
content: 'Try a different approach.',
|
||||||
|
createdAt: '2026-03-23T00:02:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg-4',
|
||||||
|
role: 'assistant',
|
||||||
|
authorName: 'Reviewer',
|
||||||
|
content: 'Here is the alternate plan.',
|
||||||
|
createdAt: '2026-03-23T00:03:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const branch = branchSessionRecord(
|
||||||
|
sourceSession,
|
||||||
|
createPattern(),
|
||||||
|
'session-branch',
|
||||||
|
'msg-2',
|
||||||
|
'2026-03-23T00:05:00.000Z',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(branch.messages.map((m) => m.id)).toEqual(['msg-1', 'msg-2']);
|
||||||
|
expect(branch.branchOrigin).toMatchObject({
|
||||||
|
sourceSessionId: 'session-1',
|
||||||
|
sourceMessageId: 'msg-2',
|
||||||
|
sourceMessageIndex: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('searches across session title, messages, projects, and patterns', () => {
|
test('searches across session title, messages, projects, and patterns', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user