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:
@@ -20,6 +20,7 @@ import type { AgentMentionInfo } from './message/types';
|
||||
import type { StreamPhase, ToolPopupContent } from './message/types';
|
||||
import { deriveMessageRole } from './message/messageRole';
|
||||
import { filterVisibleParts } from './message/partUtils';
|
||||
import { flattenAssistantTextParts } from '@/lib/messages/messageText';
|
||||
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
||||
import type { TurnGroupingContext } from './hooks/useTurnGrouping';
|
||||
|
||||
@@ -461,16 +462,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
return assistantSummaryForCopy;
|
||||
}
|
||||
|
||||
const textParts = displayParts
|
||||
.filter((part): part is Part & { type: 'text'; text?: string; content?: string } => part.type === 'text')
|
||||
.map((part) => {
|
||||
const text = part.text || part.content || '';
|
||||
return text.trim();
|
||||
})
|
||||
.filter((text) => text.length > 0);
|
||||
|
||||
const combined = textParts.join('\n');
|
||||
return combined.replace(/\n\s*\n+/g, '\n');
|
||||
return flattenAssistantTextParts(displayParts);
|
||||
}, [assistantSummaryForCopy, displayParts, isUser]);
|
||||
|
||||
const hasTextContent = messageTextContent.length > 0;
|
||||
|
||||
@@ -33,6 +33,7 @@ interface TurnDiffStats {
|
||||
export interface TurnGroupingContext {
|
||||
turnId: string;
|
||||
isFirstAssistantInTurn: boolean;
|
||||
isLastAssistantInTurn: boolean;
|
||||
|
||||
summaryBody?: string;
|
||||
|
||||
@@ -49,6 +50,7 @@ export interface TurnGroupingContext {
|
||||
markPartsPreviewed: (partIds: string[]) => void;
|
||||
}
|
||||
|
||||
|
||||
interface TurnUiState {
|
||||
isExpanded: boolean;
|
||||
previewedPartIds: Set<string>;
|
||||
@@ -361,6 +363,8 @@ export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingRe
|
||||
|
||||
const firstAssistantId = turn.assistantMessages[0]?.info.id;
|
||||
const isFirstAssistantInTurn = messageId === firstAssistantId;
|
||||
const lastAssistantId = turn.assistantMessages[turn.assistantMessages.length - 1]?.info.id;
|
||||
const isLastAssistantInTurn = messageId === lastAssistantId;
|
||||
|
||||
const uiState = getOrCreateTurnState(turn.turnId);
|
||||
const isTurnWorking = sessionIsWorking && lastTurnId === turn.turnId;
|
||||
@@ -368,6 +372,7 @@ export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingRe
|
||||
return {
|
||||
turnId: turn.turnId,
|
||||
isFirstAssistantInTurn,
|
||||
isLastAssistantInTurn,
|
||||
summaryBody,
|
||||
activityParts,
|
||||
hasTools,
|
||||
@@ -378,10 +383,12 @@ export const useTurnGrouping = (messages: ChatMessageEntry[]): UseTurnGroupingRe
|
||||
previewedPartIds: uiState.previewedPartIds,
|
||||
toggleGroup: () => toggleGroup(turn.turnId),
|
||||
markPartsPreviewed: (partIds: string[]) => markPartsPreviewedInternal(turn.turnId, partIds),
|
||||
};
|
||||
}, [getOrCreateTurnState, lastTurnId, sessionIsWorking, markPartsPreviewedInternal, messageToTurn, toggleGroup, turnActivityInfo]
|
||||
} satisfies TurnGroupingContext;
|
||||
},
|
||||
[getOrCreateTurnState, lastTurnId, markPartsPreviewedInternal, messageToTurn, sessionIsWorking, toggleGroup, turnActivityInfo]
|
||||
);
|
||||
|
||||
|
||||
return {
|
||||
turns,
|
||||
getTurnForMessage,
|
||||
|
||||
@@ -15,11 +15,14 @@ import { cn } from '@/lib/utils';
|
||||
import { isEmptyTextPart, extractTextContent } from './partUtils';
|
||||
import { FadeInOnReveal } from './FadeInOnReveal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RiCheckLine, RiFileCopyLine } from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { RiCheckLine, RiFileCopyLine, RiChatNewLine } from '@remixicon/react';
|
||||
import type { ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||
|
||||
import { SimpleMarkdownRenderer } from '../MarkdownRenderer';
|
||||
import { useMessageStore } from '@/stores/messageStore';
|
||||
import { useSessionStore } from '@/stores/sessionStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
|
||||
|
||||
const useMigrationTimer = (
|
||||
turnGroupingContext: TurnGroupingContext | undefined,
|
||||
@@ -318,6 +321,12 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
return visibleParts.filter((part) => part.type === 'text');
|
||||
}, [visibleParts]);
|
||||
|
||||
const createSessionFromAssistantMessage = useSessionStore((state) => state.createSessionFromAssistantMessage);
|
||||
const isLastAssistantInTurn = turnGroupingContext?.isLastAssistantInTurn ?? false;
|
||||
const hasStopFinish = React.useMemo(() => {
|
||||
return parts.some((part) => part.type === 'step-finish' && (part as { reason?: string | null | undefined }).reason === 'stop');
|
||||
}, [parts]);
|
||||
|
||||
const hasTools = toolParts.length > 0;
|
||||
|
||||
const hasPendingTools = React.useMemo(() => {
|
||||
@@ -511,6 +520,18 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
[hasCopyableText, isTouchContext, onCopyMessage, revealCopyHint]
|
||||
);
|
||||
|
||||
const handleForkClick = React.useCallback(
|
||||
(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (!createSessionFromAssistantMessage) {
|
||||
return;
|
||||
}
|
||||
void createSessionFromAssistantMessage(messageId);
|
||||
},
|
||||
[createSessionFromAssistantMessage, messageId]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
clearCopyHintTimeout();
|
||||
@@ -944,6 +965,8 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
summaryBody &&
|
||||
summaryBody.trim().length > 0;
|
||||
|
||||
const shouldShowFooter = hasTextContent && assistantTextParts.length > 0 && hasStopFinish && isLastAssistantInTurn;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -960,24 +983,30 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
{renderedParts}
|
||||
{showSummaryBody && (
|
||||
<FadeInOnReveal key="summary-body">
|
||||
<div
|
||||
className="group/assistant-text relative break-words"
|
||||
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
|
||||
>
|
||||
{canCopyMessage && (
|
||||
<div className="group/assistant-text relative break-words">
|
||||
<SimpleMarkdownRenderer
|
||||
content={summaryBody}
|
||||
/>
|
||||
</div>
|
||||
</FadeInOnReveal>
|
||||
)}
|
||||
</div>
|
||||
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
||||
{shouldShowFooter && (
|
||||
<div className="mt-2 mb-1 flex items-center justify-end gap-2">
|
||||
{onCopyMessage && (
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-visible={copyHintVisible || isMessageCopied ? 'true' : undefined}
|
||||
className={cn(
|
||||
'absolute z-10 flex h-7 w-7 items-center justify-center rounded-full border border-border/40 shadow-none bg-background/95 supports-[backdrop-filter]:bg-background/80 hover:bg-accent duration-150',
|
||||
'opacity-0 pointer-events-none disabled:opacity-30 disabled:text-muted-foreground/40',
|
||||
hasCopyableText &&
|
||||
'group-hover/message:opacity-60 group-hover/message:pointer-events-auto focus-visible:opacity-100 focus-visible:pointer-events-auto',
|
||||
(copyHintVisible || isMessageCopied) && 'opacity-100 pointer-events-auto'
|
||||
'h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||
!hasCopyableText && 'opacity-50',
|
||||
(copyHintVisible || isMessageCopied) && 'text-primary'
|
||||
)}
|
||||
style={{ insetInlineEnd: '0.32rem', insetBlockStart: '-0.4rem' }}
|
||||
disabled={!hasCopyableText}
|
||||
aria-label="Copy message text"
|
||||
aria-hidden={!hasCopyableText}
|
||||
@@ -997,18 +1026,30 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
|
||||
{isMessageCopied ? (
|
||||
<RiCheckLine className="h-3.5 w-3.5 text-[color:var(--status-success)]" />
|
||||
) : (
|
||||
<RiFileCopyLine className="h-3.5 w-3.5 text-foreground hover:text-primary focus-visible:text-primary" />
|
||||
<RiFileCopyLine className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<SimpleMarkdownRenderer
|
||||
content={summaryBody}
|
||||
/>
|
||||
</div>
|
||||
</FadeInOnReveal>
|
||||
)}
|
||||
</div>
|
||||
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>Copy answer</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip delayDuration={1000}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 text-muted-foreground bg-transparent hover:text-foreground hover:!bg-transparent active:!bg-transparent focus-visible:!bg-transparent focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={handleForkClick}
|
||||
>
|
||||
<RiChatNewLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>Start new session from this answer</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiChat1Line, RiCodeLine, RiGitBranchLine, RiLayoutLeftLine, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiChat4Line, RiCodeLine, RiGitBranchLine, RiLayoutLeftLine, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
|
||||
import { useUIStore, type MainTab } from '@/stores/useUIStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
@@ -233,7 +233,7 @@ export const Header: React.FC = () => {
|
||||
}, [isDesktopApp]);
|
||||
|
||||
const tabs: TabConfig[] = React.useMemo(() => [
|
||||
{ id: 'chat', label: 'Chat', icon: RiChat1Line },
|
||||
{ id: 'chat', label: 'Chat', icon: RiChat4Line },
|
||||
{ id: 'diff', label: 'Diff', icon: RiCodeLine, badge: diffFileCount > 0 ? diffFileCount : undefined },
|
||||
{ id: 'terminal', label: 'Terminal', icon: RiTerminalBoxLine },
|
||||
{ id: 'git', label: 'Git', icon: RiGitBranchLine },
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const EXECUTION_FORK_META_TEXT =
|
||||
"This message comes from an AI assistant in another session. The user wants you to respond according to its content: " +
|
||||
"if it is an implementation plan, your task is to implement that plan; " +
|
||||
"if it is a conclusion or summary, your task is to verify it, explain whether you agree or disagree, and correct it if needed. " +
|
||||
"Always clearly state what you understand your task to be, and wait for the user's approval of your conclusions before taking any further actions.";
|
||||
|
||||
export const isExecutionForkMetaText = (text: string | null | undefined): boolean =>
|
||||
typeof text === 'string' && text.trim() === EXECUTION_FORK_META_TEXT.trim();
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Part } from '@opencode-ai/sdk';
|
||||
|
||||
type TextLikePart = Part & { text?: string; content?: string };
|
||||
|
||||
export const flattenAssistantTextParts = (parts: Part[]): string => {
|
||||
const textParts = parts
|
||||
.filter((part): part is TextLikePart => part?.type === 'text')
|
||||
.map((part) => (part.text || part.content || '').trim())
|
||||
.filter((text) => text.length > 0);
|
||||
|
||||
const combined = textParts.join('\n');
|
||||
return combined.replace(/\n\s*\n+/g, '\n');
|
||||
};
|
||||
@@ -354,6 +354,7 @@ class OpencodeService {
|
||||
providerID: string;
|
||||
modelID: string;
|
||||
text: string;
|
||||
prefaceText?: string;
|
||||
agent?: string;
|
||||
files?: Array<{
|
||||
type: 'file';
|
||||
@@ -372,6 +373,13 @@ class OpencodeService {
|
||||
// Build parts array using SDK types (TextPartInput | FilePartInput) plus lightweight agent parts
|
||||
const parts: Array<TextPartInput | FilePartInput | AgentPartInputLite> = [];
|
||||
|
||||
if (params.prefaceText && params.prefaceText.trim()) {
|
||||
parts.push({
|
||||
type: 'text',
|
||||
text: params.prefaceText
|
||||
});
|
||||
}
|
||||
|
||||
// Add text part if there's content
|
||||
if (params.text && params.text.trim()) {
|
||||
const textPart: TextPartInput = {
|
||||
|
||||
@@ -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