feat: add session branching frontend UI

- Add "Branch from here" hover button on user messages in ChatPane
- Wire onBranchFromMessage to api.branchSession IPC call
- Show branch origin banner at top of branched session transcripts
- Display GitBranch indicator on branched sessions in sidebar
- Resolve source session title from workspace for origin display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 18:31:34 +02:00
co-authored by Copilot
parent 7aae1b2cd5
commit c0a37b0cd4
3 changed files with 40 additions and 0 deletions
+8
View File
@@ -571,6 +571,14 @@ export default function App() {
autoApprovedToolNames: settings.autoApprovedToolNames,
});
}}
onBranchFromMessage={(messageId) => {
void api.branchSession({ sessionId: selectedSession.id, messageId });
}}
branchOriginLabel={
selectedSession.branchOrigin
? workspace.sessions.find((s) => s.id === selectedSession.branchOrigin!.sourceSessionId)?.title
: undefined
}
availableModels={availableModels}
mcpProbingServerIds={workspace.mcpProbingServerIds}
onTerminalToggle={handleTerminalToggle}
+27
View File
@@ -64,6 +64,8 @@ interface ChatPaneProps {
}) => Promise<unknown>;
onUpdateSessionTooling?: (selection: SessionToolingSelection) => void;
onUpdateSessionApprovalSettings?: (settings: { autoApprovedToolNames?: string[] }) => void;
onBranchFromMessage?: (messageId: string) => void;
branchOriginLabel?: string;
}
export function ChatPane({
@@ -90,6 +92,8 @@ export function ChatPane({
onUpdateSessionModelConfig,
onUpdateSessionTooling,
onUpdateSessionApprovalSettings,
onBranchFromMessage,
branchOriginLabel,
}: ChatPaneProps) {
const [hasComposerContent, setHasComposerContent] = useState(false);
const [configError, setConfigError] = useState<string>();
@@ -327,6 +331,18 @@ export function ChatPane({
</div>
) : (
<div className="mx-auto max-w-3xl px-6 py-4">
{/* Branch origin banner */}
{session.branchOrigin && (
<div className="mb-4 flex items-center gap-2.5 rounded-lg border border-[var(--color-border-subtle)] bg-[var(--color-surface-1)]/60 px-3.5 py-2.5 text-[12px] text-[var(--color-text-secondary)]">
<GitBranch className="size-3.5 shrink-0 text-[var(--color-accent)]" />
<span>
Branched from{' '}
<span className="font-medium text-[var(--color-text-primary)]">
{branchOriginLabel ?? 'a previous session'}
</span>
</span>
</div>
)}
<div className="space-y-1">
{session.messages.map((message, index) => {
const isUser = message.role === 'user';
@@ -364,6 +380,17 @@ export function ChatPane({
{phaseLabel}
</span>
)}
{isUser && onBranchFromMessage && (
<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"
onClick={() => onBranchFromMessage(message.id)}
title="Branch from here — create a new session starting from this message"
type="button"
>
<GitBranch className="size-3" />
Branch
</button>
)}
</div>
<div
className={
+5
View File
@@ -296,6 +296,11 @@ function SessionItem({
Archived
</span>
)}
{session.branchOrigin && (
<span className="inline-flex items-center gap-0.5 text-[10px] text-[var(--color-text-muted)]" title="Branched session">
<GitBranch className="size-2.5" />
</span>
)}
<span className="ml-auto text-[10px] text-[var(--color-text-muted)] group-hover:text-[var(--color-text-secondary)]">
{relativeTime(session.updatedAt)}
</span>