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
+32 -18
View File
@@ -24,10 +24,7 @@ import {
type ToastState,
} from "@/components/toast-host";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import {
useWorkspaceAttachments,
useWorkspaceAttachmentScopeKey,
} from "@/attachments/workspace-attachments-store";
import { useWorkspaceAttachmentScopeKey } from "@/attachments/workspace-attachments-store";
import { COMPACT_FORM_FACTOR_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb } from "@/constants/platform";
import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear";
@@ -86,10 +83,16 @@ import { buildDraftAgentSetup, type ClientSlashCommand } from "@/client-slash-co
interface ChatAgentStateShape {
serverId: string | null;
id: string | null;
provider?: Agent["provider"];
status: Agent["status"] | null;
cwd: string | null;
workspaceId?: string;
capabilities?: Agent["capabilities"];
currentModeId?: Agent["currentModeId"];
model?: Agent["model"];
thinkingOptionId?: Agent["thinkingOptionId"];
runtimeInfo?: Agent["runtimeInfo"];
features?: Agent["features"];
lastError?: Agent["lastError"] | null;
}
@@ -130,10 +133,16 @@ function selectChatAgentState(
return {
serverId: agent.serverId,
id: agent.id,
provider: agent.provider,
status: agent.status,
cwd: agent.cwd,
workspaceId: agent.workspaceId,
capabilities: agent.capabilities,
currentModeId: agent.currentModeId,
model: agent.model,
thinkingOptionId: agent.thinkingOptionId,
runtimeInfo: agent.runtimeInfo,
features: agent.features,
lastError: agent.lastError ?? null,
archivedAt: agent.archivedAt ?? null,
requiresAttention: agent.requiresAttention ?? false,
@@ -151,10 +160,16 @@ function buildChatAgentFromState(
return {
serverId: state.serverId,
id: state.id,
provider: state.provider,
status: state.status,
cwd: state.cwd,
workspaceId: state.workspaceId,
capabilities: state.capabilities,
currentModeId: state.currentModeId,
model: state.model,
thinkingOptionId: state.thinkingOptionId,
runtimeInfo: state.runtimeInfo,
features: state.features,
lastError: state.lastError ?? null,
projectPlacement,
};
@@ -546,18 +561,7 @@ function AgentPanelBody({
(a, b) => a === b || JSON.stringify(a) === JSON.stringify(b),
);
const agentState = useSessionStore(
useShallow((state) => {
const agent = resolveChatAgentFromSession(state, serverId, agentId);
return {
serverId: agent?.serverId ?? null,
id: agent?.id ?? null,
status: agent?.status ?? null,
cwd: agent?.cwd ?? null,
workspaceId: agent?.workspaceId,
lastError: agent?.lastError ?? null,
archivedAt: agent?.archivedAt ?? null,
};
}),
useShallow((state) => selectChatAgentState(state, serverId, agentId)),
);
const [lookupState, setLookupState] = useState<AgentLookupState>({ tag: "idle" });
const lookupAttemptTokenRef = useRef(0);
@@ -644,9 +648,16 @@ function AgentPanelBody({
? {
serverId: agentState.serverId,
id: agentState.id,
provider: agentState.provider,
status: agentState.status,
cwd: agentState.cwd,
workspaceId: agentState.workspaceId,
capabilities: agentState.capabilities,
currentModeId: agentState.currentModeId,
model: agentState.model,
thinkingOptionId: agentState.thinkingOptionId,
runtimeInfo: agentState.runtimeInfo,
features: agentState.features,
lastError: agentState.lastError ?? null,
projectPlacement,
}
@@ -1378,7 +1389,10 @@ function ActiveAgentComposer({
cwd,
workspaceId,
});
const workspaceAttachments = useWorkspaceAttachments(workspaceAttachmentScopeKey);
const attachmentScopeKeys = useMemo(
() => [workspaceAttachmentScopeKey],
[workspaceAttachmentScopeKey],
);
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
const handleOpenWorkspaceAttachment = useCallback(
@@ -1479,7 +1493,7 @@ function ActiveAgentComposer({
value={agentInputDraft.text}
onChangeText={agentInputDraft.setText}
attachments={agentInputDraft.attachments}
workspaceAttachments={workspaceAttachments}
attachmentScopeKeys={attachmentScopeKeys}
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
onChangeAttachments={agentInputDraft.setAttachments}
cwd={cwd}