feat: add fork assistant message to new session
Add fork button on assistant messages to start new execution session Include synthetic meta-instruction when forking to explain context
This commit is contained in:
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
||||
import { devtools, persist, createJSONStorage } from "zustand/middleware";
|
||||
import type { Message, Part } from "@opencode-ai/sdk";
|
||||
import { opencodeClient } from "@/lib/opencode/client";
|
||||
import { isExecutionForkMetaText } from "@/lib/messages/executionMeta";
|
||||
import type { SessionMemoryState, MessageStreamLifecycle, AttachedFile } from "./types/sessionTypes";
|
||||
import { MEMORY_LIMITS } from "./types/sessionTypes";
|
||||
import {
|
||||
@@ -332,7 +333,15 @@ export const useMessageStore = create<MessageStore>()(
|
||||
userMessageMarker: message.info.role === "user" ? true : (message.info as any)?.userMessageMarker,
|
||||
} as any;
|
||||
|
||||
const serverParts = Array.isArray(message.parts) ? [...message.parts] : [];
|
||||
const serverParts = (Array.isArray(message.parts) ? message.parts : []).map((part) => {
|
||||
if (part?.type === 'text') {
|
||||
const raw = (part as any).text ?? (part as any).content ?? '';
|
||||
if (isExecutionForkMetaText(raw)) {
|
||||
return { ...part, synthetic: true } as Part;
|
||||
}
|
||||
}
|
||||
return part;
|
||||
});
|
||||
const existingEntry = infoWithMarker?.id
|
||||
? previousMessagesById.get(infoWithMarker.id as string)
|
||||
: undefined;
|
||||
@@ -935,6 +944,9 @@ export const useMessageStore = create<MessageStore>()(
|
||||
}
|
||||
|
||||
const incomingText = extractTextFromPart(part);
|
||||
if (isExecutionForkMetaText(incomingText)) {
|
||||
(part as any).synthetic = true;
|
||||
}
|
||||
if (streamDebugEnabled() && actualRole === "assistant") {
|
||||
try {
|
||||
console.info("[STREAM-TRACE] part", {
|
||||
@@ -1789,7 +1801,15 @@ export const useMessageStore = create<MessageStore>()(
|
||||
: (message.info as any)?.animationSettled,
|
||||
} as any;
|
||||
|
||||
const serverParts = Array.isArray(message.parts) ? [...message.parts] : [];
|
||||
const serverParts = (Array.isArray(message.parts) ? message.parts : []).map((part) => {
|
||||
if (part?.type === 'text') {
|
||||
const raw = (part as any).text ?? (part as any).content ?? '';
|
||||
if (isExecutionForkMetaText(raw)) {
|
||||
return { ...part, synthetic: true } as Part;
|
||||
}
|
||||
}
|
||||
return part;
|
||||
});
|
||||
const messageId = typeof infoWithMarker?.id === "string" ? (infoWithMarker.id as string) : undefined;
|
||||
const existingEntry = messageId ? previousMessagesById.get(messageId) : undefined;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ interface SessionState {
|
||||
|
||||
interface SessionActions {
|
||||
loadSessions: () => Promise<void>;
|
||||
createSession: (title?: string, directoryOverride?: string | null) => Promise<Session | null>;
|
||||
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise<Session | null>;
|
||||
deleteSession: (id: string, options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; remoteName?: string }) => Promise<boolean>;
|
||||
deleteSessions: (ids: string[], options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; remoteName?: string }) => Promise<{ deletedIds: string[]; failedIds: string[] }>;
|
||||
updateSessionTitle: (id: string, title: string) => Promise<void>;
|
||||
@@ -449,7 +449,7 @@ export const useSessionStore = create<SessionStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
createSession: async (title?: string, directoryOverride?: string | null) => {
|
||||
createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => {
|
||||
set({ error: null });
|
||||
const directoryStore = useDirectoryStore.getState();
|
||||
const fallbackDirectory = normalizePath(directoryStore.currentDirectory);
|
||||
@@ -461,7 +461,7 @@ export const useSessionStore = create<SessionStore>()(
|
||||
const optimisticSession: Session = {
|
||||
id: tempId,
|
||||
title: title || "New session",
|
||||
parentID: undefined,
|
||||
parentID: parentID ?? undefined,
|
||||
directory: targetDirectory ?? null,
|
||||
projectID: (previousState.sessions[0] as { projectID?: string })?.projectID ?? "",
|
||||
version: "0.0.0",
|
||||
@@ -529,7 +529,7 @@ export const useSessionStore = create<SessionStore>()(
|
||||
};
|
||||
|
||||
try {
|
||||
const createRequest = () => opencodeClient.createSession({ title });
|
||||
const createRequest = () => opencodeClient.createSession({ title, parentID: parentID ?? undefined });
|
||||
let session: Session | null = null;
|
||||
|
||||
try {
|
||||
|
||||
@@ -100,7 +100,8 @@ export interface SessionStore {
|
||||
toggleSessionAgentEditMode: (sessionId: string, agentName: string | undefined, defaultMode?: EditPermissionMode) => void;
|
||||
setSessionAgentEditMode: (sessionId: string, agentName: string | undefined, mode: EditPermissionMode, defaultMode?: EditPermissionMode) => void;
|
||||
loadSessions: () => Promise<void>;
|
||||
createSession: (title?: string, directoryOverride?: string | null) => Promise<Session | null>;
|
||||
createSession: (title?: string, directoryOverride?: string | null, parentID?: string | null) => Promise<Session | null>;
|
||||
createSessionFromAssistantMessage: (sourceMessageId: string) => Promise<void>;
|
||||
|
||||
deleteSession: (id: string, options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; remoteName?: string }) => Promise<boolean>;
|
||||
deleteSessions: (ids: string[], options?: { archiveWorktree?: boolean; deleteRemoteBranch?: boolean; remoteName?: string }) => Promise<{ deletedIds: string[]; failedIds: string[] }>;
|
||||
|
||||
@@ -13,6 +13,9 @@ import { useContextStore } from "./contextStore";
|
||||
import { usePermissionStore } from "./permissionStore";
|
||||
import { opencodeClient } from "@/lib/opencode/client";
|
||||
import { useDirectoryStore } from "./useDirectoryStore";
|
||||
import { useConfigStore } from "./useConfigStore";
|
||||
import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta";
|
||||
import { flattenAssistantTextParts } from "@/lib/messages/messageText";
|
||||
|
||||
export type { AttachedFile, EditPermissionMode };
|
||||
export { MEMORY_LIMITS, ACTIVE_SESSION_WINDOW } from "./types/sessionTypes";
|
||||
@@ -105,14 +108,70 @@ export const useSessionStore = create<SessionStore>()(
|
||||
},
|
||||
|
||||
loadSessions: () => useSessionManagementStore.getState().loadSessions(),
|
||||
createSession: async (title?: string, directoryOverride?: string | null) => {
|
||||
const result = await useSessionManagementStore.getState().createSession(title, directoryOverride);
|
||||
createSession: async (title?: string, directoryOverride?: string | null, parentID?: string | null) => {
|
||||
const result = await useSessionManagementStore.getState().createSession(title, directoryOverride, parentID);
|
||||
|
||||
if (result?.id) {
|
||||
await get().setCurrentSession(result.id);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
createSessionFromAssistantMessage: async (sourceMessageId: string) => {
|
||||
if (!sourceMessageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageStore = useMessageStore.getState();
|
||||
const { messages, lastUsedProvider } = messageStore;
|
||||
let sourceEntry: { info: Message; parts: Part[] } | undefined;
|
||||
let sourceSessionId: string | undefined;
|
||||
|
||||
messages.forEach((messageList, sessionId) => {
|
||||
const found = messageList.find((entry) => entry.info?.id === sourceMessageId);
|
||||
if (found && !sourceEntry) {
|
||||
sourceEntry = found;
|
||||
sourceSessionId = sessionId;
|
||||
}
|
||||
});
|
||||
|
||||
if (!sourceEntry || sourceEntry.info.role !== "assistant") {
|
||||
return;
|
||||
}
|
||||
|
||||
const assistantPlanText = flattenAssistantTextParts(sourceEntry.parts);
|
||||
if (!assistantPlanText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionManagementStore = useSessionManagementStore.getState();
|
||||
const directory = resolveSessionDirectory(
|
||||
sessionManagementStore.sessions,
|
||||
sourceSessionId ?? null,
|
||||
sessionManagementStore.getWorktreeMetadata,
|
||||
);
|
||||
|
||||
const session = await get().createSession(undefined, directory ?? null, null);
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { currentProviderId, currentModelId, currentAgentName } = useConfigStore.getState();
|
||||
const providerID = currentProviderId || lastUsedProvider?.providerID;
|
||||
const modelID = currentModelId || lastUsedProvider?.modelID;
|
||||
|
||||
if (!providerID || !modelID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await opencodeClient.sendMessage({
|
||||
id: session.id,
|
||||
providerID,
|
||||
modelID,
|
||||
text: assistantPlanText,
|
||||
prefaceText: EXECUTION_FORK_META_TEXT,
|
||||
agent: currentAgentName ?? undefined,
|
||||
});
|
||||
},
|
||||
deleteSession: (id: string, options) => useSessionManagementStore.getState().deleteSession(id, options),
|
||||
deleteSessions: (ids: string[], options) => useSessionManagementStore.getState().deleteSessions(ids, options),
|
||||
updateSessionTitle: (id: string, title: string) => useSessionManagementStore.getState().updateSessionTitle(id, title),
|
||||
|
||||
Reference in New Issue
Block a user