Fork assistant turns into new drafts (#1788)

* feat(chat): fork assistant turns into new drafts

Builds chat-history attachments on the daemon and routes them into draft composers so users can continue an assistant turn in an existing workspace tab or New Workspace.

* fix(chat): clean up fork draft handling

* fix(chat): address fork draft review feedback

* fix(server): build fork context from bounded timeline

* fix(app): restore assistant turn footer in streams

* fix(app): preserve fork draft setup

* fix(app): tighten assistant fork footers

* fix(app): lock active fork draft handoff

* fix(app): preserve attachment-only draft retries

* fix(app): unify composer attachment scopes

* fix(app): align chat history attachment labels

* fix(app): centralize attachment pill content

* fix(app): trim fork context for workspace naming
This commit is contained in:
Mohamed Boudra
2026-06-29 18:29:59 +02:00
committed by GitHub
parent e63a971968
commit 85acaceb16
54 changed files with 2364 additions and 312 deletions
@@ -289,4 +289,67 @@ describe("layoutStream", () => {
expect(findLayoutItem(layout, assistant.id).completedFooter).toBeNull();
expect(footerOwners(layout)).toEqual([assistant.id]);
});
it.each(["web", "android"] as const)(
"keeps inline footer on an assistant turn with trailing tool rows before the next user on %s",
(platform) => {
const assistant = assistantMessage("a1", 2);
const tool = toolCall("tool-1", 3);
const layout = layoutFor({
platform,
tail: [userMessage("u1", 1), assistant, tool, userMessage("u2", 4)],
timingIds: [assistant.id],
});
expect(layout.auxiliaryTurnFooter).toBeNull();
expect(findLayoutItem(layout, assistant.id).completedFooter?.itemId).toBe(assistant.id);
expect(footerOwners(layout)).toEqual([assistant.id]);
},
);
it.each(["web", "android"] as const)(
"uses the latest assistant for an inline footer when a turn has multiple assistant blocks on %s",
(platform) => {
const firstAssistant = assistantMessage("a1", 2);
const firstTool = toolCall("tool-1", 3);
const latestAssistant = assistantMessage("a2", 4);
const latestTool = toolCall("tool-2", 5);
const layout = layoutFor({
platform,
tail: [
userMessage("u1", 1),
firstAssistant,
firstTool,
latestAssistant,
latestTool,
userMessage("u2", 6),
],
timingIds: [firstAssistant.id, latestAssistant.id],
});
expect(layout.auxiliaryTurnFooter).toBeNull();
expect(findLayoutItem(layout, firstAssistant.id).completedFooter).toBeNull();
expect(findLayoutItem(layout, latestAssistant.id).completedFooter?.itemId).toBe(
latestAssistant.id,
);
expect(footerOwners(layout)).toEqual([latestAssistant.id]);
},
);
it.each(["web", "android"] as const)(
"keeps bottom footer on the latest assistant turn when trailing tool rows end the turn on %s",
(platform) => {
const assistant = assistantMessage("a1", 2);
const tool = toolCall("tool-1", 3);
const layout = layoutFor({
platform,
tail: [userMessage("u1", 1), assistant, tool],
timingIds: [assistant.id],
});
expect(layout.auxiliaryTurnFooter?.itemId).toBe(assistant.id);
expect(findLayoutItem(layout, assistant.id).completedFooter).toBeNull();
expect(footerOwners(layout)).toEqual([assistant.id]);
},
);
});