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
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from "vitest";
import { useCreateFlowStore } from "./create-flow-store";
import { isActiveCreateFlowForDraft, useCreateFlowStore } from "./create-flow-store";
describe("create-flow-store", () => {
beforeEach(() => {
@@ -88,4 +88,40 @@ describe("create-flow-store", () => {
expect(useCreateFlowStore.getState().pendingByDraftId["draft-1"]).toBeUndefined();
});
it("matches only active pending create flows for a draft and server", () => {
useCreateFlowStore.getState().setPending({
draftId: "draft-1",
serverId: "server-1",
agentId: null,
clientMessageId: "msg-1",
text: "hello",
timestamp: Date.now(),
});
const pending = useCreateFlowStore.getState().pendingByDraftId["draft-1"];
expect(
isActiveCreateFlowForDraft({
pending,
serverId: "server-1",
draftId: " draft-1 ",
}),
).toBe(true);
expect(
isActiveCreateFlowForDraft({
pending,
serverId: "server-2",
draftId: "draft-1",
}),
).toBe(false);
useCreateFlowStore.getState().markLifecycle({ draftId: "draft-1", lifecycle: "sent" });
expect(
isActiveCreateFlowForDraft({
pending: useCreateFlowStore.getState().pendingByDraftId["draft-1"],
serverId: "server-1",
draftId: "draft-1",
}),
).toBe(false);
});
});
@@ -17,6 +17,20 @@ export interface PendingCreateAttempt {
attachments?: AgentAttachment[];
}
export function isActiveCreateFlowForDraft(input: {
pending: PendingCreateAttempt | null | undefined;
serverId: string;
draftId: string | null | undefined;
}): boolean {
const draftId = input.draftId?.trim();
return Boolean(
draftId &&
input.pending?.draftId === draftId &&
input.pending.serverId === input.serverId &&
input.pending.lifecycle === "active",
);
}
interface CreateFlowState {
pendingByDraftId: Record<string, PendingCreateAttempt>;
setPending: (pending: Omit<PendingCreateAttempt, "lifecycle">) => void;
@@ -1,6 +1,7 @@
import { create } from "zustand";
import type { ComposerAttachment } from "@/attachments/types";
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
import type { WorkspaceDraftTabSetup } from "@/stores/workspace-tabs-store";
export interface PendingWorkspaceDraftSubmission {
serverId: string;
@@ -19,9 +20,21 @@ export interface PendingWorkspaceDraftSubmission {
allowEmptyText?: boolean;
}
export interface PendingWorkspaceDraftSetup {
setup: WorkspaceDraftTabSetup;
sourceDirectory?: string | null;
}
interface WorkspaceDraftSubmissionState {
pendingByDraftId: Record<string, PendingWorkspaceDraftSubmission>;
setupByDraftId: Record<string, PendingWorkspaceDraftSetup>;
setPending: (submission: PendingWorkspaceDraftSubmission) => void;
setDraftSetup: (input: {
draftId: string;
setup: WorkspaceDraftTabSetup;
sourceDirectory?: string | null;
}) => void;
clearDraftSetup: (input: { draftId: string }) => void;
consumePending: (input: {
serverId: string;
workspaceId: string;
@@ -40,9 +53,14 @@ function matchesPendingSubmission(
);
}
function normalizeDraftId(draftId: string): string {
return draftId.trim();
}
export const useWorkspaceDraftSubmissionStore = create<WorkspaceDraftSubmissionState>(
(set, get) => ({
pendingByDraftId: {},
setupByDraftId: {},
setPending: (submission) =>
set((state) => ({
pendingByDraftId: {
@@ -50,6 +68,25 @@ export const useWorkspaceDraftSubmissionStore = create<WorkspaceDraftSubmissionS
[submission.draftId]: submission,
},
})),
setDraftSetup: ({ draftId, setup, sourceDirectory }) => {
const normalizedDraftId = normalizeDraftId(draftId);
if (!normalizedDraftId) return;
set((state) => ({
setupByDraftId: {
...state.setupByDraftId,
[normalizedDraftId]: { setup, sourceDirectory: sourceDirectory ?? null },
},
}));
},
clearDraftSetup: ({ draftId }) => {
const normalizedDraftId = normalizeDraftId(draftId);
if (!normalizedDraftId) return;
set((state) => {
if (!state.setupByDraftId[normalizedDraftId]) return state;
const { [normalizedDraftId]: _removed, ...setupByDraftId } = state.setupByDraftId;
return { setupByDraftId };
});
},
consumePending: (input) => {
const pending = get().pendingByDraftId[input.draftId];
if (!matchesPendingSubmission(pending, input)) {