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
+41
View File
@@ -26,6 +26,7 @@ import type {
FileUploadResponse,
FileExplorerResponse,
FetchAgentTimelineResponseMessage,
AgentForkContextResponseMessage,
GitSetupOptions,
CheckoutStatusResponse,
CheckoutCommitResponse,
@@ -456,6 +457,7 @@ type ScheduleUpdatePayload = Extract<
{ type: "schedule/update/response" }
>["payload"];
export type FetchAgentTimelinePayload = FetchAgentTimelineResponseMessage["payload"];
export type AgentForkContextPayload = AgentForkContextResponseMessage["payload"];
export type FetchAgentTimelineDirection = FetchAgentTimelinePayload["direction"];
export type FetchAgentTimelineProjection = FetchAgentTimelinePayload["projection"];
@@ -502,6 +504,10 @@ function normalizeListCommandsOptions(
}
return { agentId: input, ...legacyOptions };
}
export interface AgentForkContextOptions {
boundaryMessageId?: string;
requestId?: string;
}
type AgentRefreshedStatusPayload = z.infer<typeof AgentRefreshedStatusPayloadSchema>;
type RestartRequestedStatusPayload = z.infer<typeof RestartRequestedStatusPayloadSchema>;
@@ -2405,6 +2411,41 @@ export class DaemonClient {
return payload;
}
async buildAgentForkContext(
agentId: string,
options: AgentForkContextOptions = {},
): Promise<AgentForkContextPayload> {
const resolvedRequestId = this.createRequestId(options.requestId);
const message = SessionInboundMessageSchema.parse({
type: "agent.fork_context.request",
agentId,
requestId: resolvedRequestId,
...(options.boundaryMessageId ? { boundaryMessageId: options.boundaryMessageId } : {}),
});
const payload = await this.sendRequest({
requestId: resolvedRequestId,
message,
timeout: 15000,
options: { skipQueue: true },
select: (msg) => {
if (msg.type !== "agent.fork_context.response") {
return null;
}
if (msg.payload.requestId !== resolvedRequestId) {
return null;
}
return msg.payload;
},
});
if (payload.error) {
throw new Error(payload.error);
}
return payload;
}
// ============================================================================
// Agent Interaction
// ============================================================================