Merge remote-tracking branch 'origin/main'
# Conflicts: # .project-docs/20-architecture/module-map.md # src/pages/ImageCanvas/DesignConversationPane.tsx # tests/unit/image-canvas-page.test.tsx
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Clock3,
|
||||
Loader2,
|
||||
Send,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import makeloreMark from '@/assets/logo.svg';
|
||||
@@ -10,7 +13,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import {
|
||||
type DesignAssistantActivity,
|
||||
useImageWorkspaceStore,
|
||||
} from '@/stores/image-workspace';
|
||||
import type { DesignCompilationIssue, DesignWorkspace } from '../../../shared/image-workspace';
|
||||
import { DesignPendingOperationsPanel } from './DesignProductionPanel';
|
||||
import { DesignPlanHistory } from './DesignPlanHistory';
|
||||
@@ -22,6 +28,83 @@ const STARTER_MESSAGES = [
|
||||
'把我的涂鸦变成一段会动的小故事',
|
||||
];
|
||||
|
||||
function DesignAssistantActivityPanel({
|
||||
activity,
|
||||
operationId,
|
||||
}: {
|
||||
activity: DesignAssistantActivity;
|
||||
operationId: string;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(activity.status !== 'completed');
|
||||
|
||||
const summary = activity.status === 'active'
|
||||
? `正在进行第 ${activity.steps.length} 个步骤`
|
||||
: activity.status === 'completed'
|
||||
? `完成了 ${activity.steps.length} 个步骤`
|
||||
: '连接暂时中断,已完成的步骤还在这里';
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid={`design-assistant-activity-${operationId}`}
|
||||
aria-label="AI 处理过程"
|
||||
aria-live="polite"
|
||||
className="mr-auto w-full max-w-[92%] overflow-hidden rounded-xl border border-brand/15 bg-brand/[0.035]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
className="flex min-h-11 w-full items-center gap-3 px-3.5 py-2.5 text-left transition-colors duration-150 hover:bg-brand/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand/25 motion-reduce:transition-none"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
>
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-background text-brand shadow-sm">
|
||||
{activity.status === 'active'
|
||||
? <Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
: activity.status === 'completed'
|
||||
? <Check className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
: <Clock3 className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-xs font-semibold text-foreground">AI 处理过程</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground">
|
||||
{summary}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-150 motion-reduce:transition-none',
|
||||
expanded && 'rotate-180',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<ol className="space-y-2 border-t border-brand/10 px-3.5 py-3">
|
||||
{activity.steps.map((step, index) => {
|
||||
const isCurrent = activity.status === 'active' && index === activity.steps.length - 1;
|
||||
const isUncertain = activity.status === 'unknown' && index === activity.steps.length - 1;
|
||||
return (
|
||||
<li
|
||||
key={step.stage}
|
||||
className="flex min-h-6 items-start gap-2.5 text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
<span className="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center text-brand">
|
||||
{isCurrent
|
||||
? <Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" aria-hidden="true" />
|
||||
: isUncertain
|
||||
? <Clock3 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
: <Check className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
</span>
|
||||
<span>{step.message}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function chatFailureMessage(error: unknown): string {
|
||||
if (error instanceof ImageWorkspaceApiError) {
|
||||
if (error.commandOutcome === 'unknown') {
|
||||
@@ -62,6 +145,7 @@ export function DesignConversationPane({
|
||||
const chatDraft = useImageWorkspaceStore((state) => state.chatDraft);
|
||||
const setChatDraft = useImageWorkspaceStore((state) => state.setChatDraft);
|
||||
const sendChat = useImageWorkspaceStore((state) => state.sendChat);
|
||||
const assistantActivities = useImageWorkspaceStore((state) => state.assistantActivities);
|
||||
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
|
||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
||||
@@ -79,12 +163,17 @@ export function DesignConversationPane({
|
||||
}];
|
||||
});
|
||||
const submittingDesignInput = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
|
||||
(operation) => operation.status === 'submitting'
|
||||
&& operation.command.kind === 'apply_input'
|
||||
&& operation.command.workspaceId === workspace.workspace.workspaceId,
|
||||
);
|
||||
const streamingReplies = pendingChatMessages.flatMap((message) => {
|
||||
const text = assistantStreams[message.operationId]?.trim();
|
||||
return text ? [{ ...message, text }] : [];
|
||||
});
|
||||
const streamingReplyByOperationId = new Map(
|
||||
streamingReplies.map((reply) => [reply.operationId, reply.text]),
|
||||
);
|
||||
const projectedDraft = chatDraft.trim() !== ''
|
||||
&& pendingChatMessages.some((message) => message.message === chatDraft.trim());
|
||||
const composerDraft = projectedDraft ? '' : chatDraft;
|
||||
@@ -94,12 +183,29 @@ export function DesignConversationPane({
|
||||
const streamingReplyKey = streamingReplies
|
||||
.map((reply) => `${reply.operationId}:${reply.text}`)
|
||||
.join('|');
|
||||
const visibleActivities = Object.entries(assistantActivities).filter(([, activity]) => (
|
||||
activity.workspaceId === workspace.workspace.workspaceId
|
||||
&& activity.directionId === workspace.form.directionId
|
||||
&& activity.steps.length > 0
|
||||
));
|
||||
const activityByOperationId = new Map(visibleActivities);
|
||||
const activityByTurnId = new Map(
|
||||
visibleActivities.flatMap(([operationId, activity]) => (
|
||||
activity.turnId ? [[activity.turnId, { operationId, activity }] as const] : []
|
||||
)),
|
||||
);
|
||||
const activityKey = visibleActivities
|
||||
.map(([operationId, activity]) => (
|
||||
`${operationId}:${activity.status}:${activity.steps.map((step) => step.stage).join(',')}`
|
||||
))
|
||||
.join('|');
|
||||
useEffect(() => {
|
||||
const scrollAnchor = scrollAnchorRef.current;
|
||||
if (typeof scrollAnchor?.scrollIntoView === 'function') {
|
||||
scrollAnchor.scrollIntoView({ block: 'end' });
|
||||
}
|
||||
}, [
|
||||
activityKey,
|
||||
pendingChatKey,
|
||||
streamingReplyKey,
|
||||
workspace.form.specificationRevision,
|
||||
@@ -152,57 +258,80 @@ export function DesignConversationPane({
|
||||
)}
|
||||
|
||||
<div className="mx-auto max-w-[50rem] space-y-6">
|
||||
{workspace.turns.map((turn) => (
|
||||
<div key={turn.turnId} className="space-y-5">
|
||||
{turn.userMessage && (
|
||||
<div className="chat-user-message-surface ml-auto max-w-[82%] whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.userMessage}
|
||||
</div>
|
||||
)}
|
||||
{turn.assistantMessage && (
|
||||
<div className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.assistantMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{pendingChatMessages.map((message) => (
|
||||
<div
|
||||
key={message.operationId}
|
||||
data-testid={`pending-design-chat-${message.operationId}`}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="ml-auto max-w-[82%]"
|
||||
>
|
||||
<div className="chat-user-message-surface whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{message.message}
|
||||
{workspace.turns.map((turn) => {
|
||||
const linkedActivity = activityByTurnId.get(turn.turnId);
|
||||
return (
|
||||
<div key={turn.turnId} className="space-y-5">
|
||||
{turn.userMessage && (
|
||||
<div className="chat-user-message-surface ml-auto max-w-[82%] whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.userMessage}
|
||||
</div>
|
||||
)}
|
||||
{linkedActivity && (
|
||||
<DesignAssistantActivityPanel
|
||||
key={`${linkedActivity.operationId}:${linkedActivity.activity.status}`}
|
||||
activity={linkedActivity.activity}
|
||||
operationId={linkedActivity.operationId}
|
||||
/>
|
||||
)}
|
||||
{turn.assistantMessage && (
|
||||
<div className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{turn.assistantMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-end gap-1.5 pr-1 text-[11px] text-muted-foreground">
|
||||
{message.status === 'submitting'
|
||||
? <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
|
||||
: <Clock3 className="h-3 w-3" aria-hidden="true" />}
|
||||
<span>{message.status === 'submitting' ? '发送中' : '正在确认'}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{streamingReplies.map((reply) => (
|
||||
<div
|
||||
key={reply.operationId}
|
||||
data-testid={`streaming-design-assistant-${reply.operationId}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground"
|
||||
>
|
||||
{reply.text}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-1 inline-block h-3.5 w-1 animate-pulse rounded-full bg-brand align-middle"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{pendingChatMessages.map((message) => {
|
||||
const linkedActivity = activityByOperationId.get(message.operationId);
|
||||
const streamingReply = streamingReplyByOperationId.get(message.operationId);
|
||||
return (
|
||||
<div
|
||||
key={message.operationId}
|
||||
className="space-y-2.5"
|
||||
>
|
||||
<div
|
||||
data-testid={`pending-design-chat-${message.operationId}`}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="ml-auto max-w-[82%]"
|
||||
>
|
||||
<div className="chat-user-message-surface whitespace-pre-wrap break-words rounded-[18px] rounded-tr-md px-3.5 py-2.5 text-pretty text-[15px] leading-6 text-foreground">
|
||||
{message.message}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center justify-end gap-1.5 pr-1 text-[11px] text-muted-foreground">
|
||||
{message.status === 'submitting'
|
||||
? <Loader2 className="h-3 w-3 animate-spin" aria-hidden="true" />
|
||||
: <Clock3 className="h-3 w-3" aria-hidden="true" />}
|
||||
<span>{message.status === 'submitting' ? '发送中' : '正在确认'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{linkedActivity && (
|
||||
<DesignAssistantActivityPanel
|
||||
key={`${message.operationId}:${linkedActivity.status}`}
|
||||
activity={linkedActivity}
|
||||
operationId={message.operationId}
|
||||
/>
|
||||
)}
|
||||
{streamingReply && (
|
||||
<div
|
||||
data-testid={`streaming-design-assistant-${message.operationId}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="chat-assistant-message-surface mr-auto w-full whitespace-pre-wrap break-words py-0.5 text-pretty text-[15px] leading-6 text-foreground"
|
||||
>
|
||||
{streamingReply}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="ml-1 inline-block h-3.5 w-1 animate-pulse rounded-full bg-brand align-middle"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<DesignPendingOperationsPanel />
|
||||
{showPlanHistory ? <DesignPlanHistory workspace={workspace} /> : null}
|
||||
|
||||
@@ -15,6 +15,9 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import {
|
||||
IMAGE_WORKSPACE_UNAVAILABLE_CODE,
|
||||
type DesignCommandInput,
|
||||
type DesignAssistantProgressEvent,
|
||||
type DesignAssistantProgressStage,
|
||||
type DesignCommandTerminalEvent,
|
||||
type DesignCompilationIssue,
|
||||
type DesignDeleteWorkspaceResult,
|
||||
type DesignFieldChange,
|
||||
@@ -44,6 +47,19 @@ export type PendingDesignOperation = {
|
||||
error: string | null;
|
||||
clearDraftPaths: string[];
|
||||
clearChatDraft: boolean;
|
||||
baseRawTurnSequence: number | null;
|
||||
workspaceSelectionGeneration?: number;
|
||||
};
|
||||
|
||||
export type DesignAssistantActivity = {
|
||||
workspaceId: string;
|
||||
directionId: string;
|
||||
status: 'active' | 'completed' | 'unknown';
|
||||
turnId: string | null;
|
||||
steps: Array<{
|
||||
stage: DesignAssistantProgressStage;
|
||||
message: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ExecuteCommandOptions = {
|
||||
@@ -63,6 +79,7 @@ type ImageWorkspaceState = {
|
||||
pendingOperations: Record<string, PendingDesignOperation>;
|
||||
fieldDrafts: Record<string, unknown>;
|
||||
chatDraft: string;
|
||||
assistantActivities: Record<string, DesignAssistantActivity>;
|
||||
assistantStreams: Record<string, string>;
|
||||
quoteBlockers: DesignCompilationIssue[];
|
||||
error: string | null;
|
||||
@@ -98,7 +115,15 @@ let activeEventSource: EventSource | null = null;
|
||||
let activeEventWorkspaceId: string | null = null;
|
||||
let eventConnectionGeneration = 0;
|
||||
let selectionGeneration = 0;
|
||||
let workspaceSelectionGeneration = 0;
|
||||
const assistantStreamChunkIndexes = new Map<string, number>();
|
||||
const observedCommandTerminalEvents = new Map<string, DesignCommandTerminalEvent>();
|
||||
const ASSISTANT_PROGRESS_STAGES: DesignAssistantProgressStage[] = [
|
||||
'understanding',
|
||||
'reviewing_context',
|
||||
'validating',
|
||||
'composing',
|
||||
];
|
||||
|
||||
function updateBackgroundLease(tasks: DesignGenerationTask[]): void {
|
||||
const active = tasks.some((task) => task.status === 'queued' || task.status === 'running');
|
||||
@@ -161,6 +186,140 @@ function isUnknownCommandOutcome(error: unknown): boolean {
|
||||
return error instanceof ImageWorkspaceApiError && error.commandOutcome === 'unknown';
|
||||
}
|
||||
|
||||
function terminalCommandError(event: DesignCommandTerminalEvent): ImageWorkspaceApiError {
|
||||
const cancelled = event.outcome === 'cancelled';
|
||||
return new ImageWorkspaceApiError(
|
||||
422,
|
||||
event.errorCode ?? (cancelled ? 'design_agent_run_cancelled' : 'design_agent_run_failed'),
|
||||
cancelled
|
||||
? '这次整理已停止,内容还在输入框里'
|
||||
: 'AI 这次没有完成设计整理,内容还在输入框里,请再试一次',
|
||||
'definitive_failure',
|
||||
);
|
||||
}
|
||||
|
||||
function isChatCommand(command: DesignCommandInput): boolean {
|
||||
return command.kind === 'apply_input' && command.input.kind === 'chat';
|
||||
}
|
||||
|
||||
function matchingChatTurn(
|
||||
workspace: DesignWorkspace,
|
||||
operation: PendingDesignOperation,
|
||||
pendingOperations: Record<string, PendingDesignOperation>,
|
||||
activity: DesignAssistantActivity | undefined,
|
||||
): DesignWorkspace['turns'][number] | null {
|
||||
if (activity?.turnId) {
|
||||
return workspace.turns.find((turn) => turn.turnId === activity.turnId) ?? null;
|
||||
}
|
||||
const baseRawTurnSequence = operation.baseRawTurnSequence;
|
||||
const command = operation.command;
|
||||
if (baseRawTurnSequence === null
|
||||
|| command.kind !== 'apply_input'
|
||||
|| command.input.kind !== 'chat') return null;
|
||||
const message = command.input.message;
|
||||
const competingOperation = Object.values(pendingOperations).some((candidate) => (
|
||||
candidate.id !== operation.id
|
||||
&& candidate.baseRawTurnSequence === baseRawTurnSequence
|
||||
&& candidate.command.kind === 'apply_input'
|
||||
&& candidate.command.input.kind === 'chat'
|
||||
&& candidate.command.input.message === message
|
||||
));
|
||||
if (competingOperation) return null;
|
||||
const matches = workspace.turns
|
||||
.filter((turn) => (
|
||||
turn.rawTurnSequence > baseRawTurnSequence
|
||||
&& turn.userMessage === message
|
||||
))
|
||||
.sort((left, right) => left.rawTurnSequence - right.rawTurnSequence);
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
}
|
||||
|
||||
function isCurrentOperationSelection(
|
||||
state: Pick<ImageWorkspaceState, 'activeWorkspaceId'>,
|
||||
operation: PendingDesignOperation,
|
||||
): boolean {
|
||||
return state.activeWorkspaceId === operation.command.workspaceId
|
||||
&& (operation.workspaceSelectionGeneration ?? workspaceSelectionGeneration)
|
||||
=== workspaceSelectionGeneration;
|
||||
}
|
||||
|
||||
function withoutAssistantActivity(
|
||||
activities: Record<string, DesignAssistantActivity>,
|
||||
operationId: string,
|
||||
): Record<string, DesignAssistantActivity> {
|
||||
if (!activities[operationId]) return activities;
|
||||
const next = { ...activities };
|
||||
delete next[operationId];
|
||||
return next;
|
||||
}
|
||||
|
||||
function withAssistantActivityStatus(
|
||||
activities: Record<string, DesignAssistantActivity>,
|
||||
operationId: string,
|
||||
status: DesignAssistantActivity['status'],
|
||||
turnId?: string | null,
|
||||
): Record<string, DesignAssistantActivity> {
|
||||
const activity = activities[operationId];
|
||||
if (!activity) return activities;
|
||||
return {
|
||||
...activities,
|
||||
[operationId]: {
|
||||
...activity,
|
||||
status,
|
||||
...(turnId === undefined ? {} : { turnId }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withAssistantProgress(
|
||||
activities: Record<string, DesignAssistantActivity>,
|
||||
event: DesignAssistantProgressEvent,
|
||||
): Record<string, DesignAssistantActivity> {
|
||||
const activity = activities[event.clientOperationId];
|
||||
if (activity?.status === 'completed') return activities;
|
||||
const incomingRank = ASSISTANT_PROGRESS_STAGES.indexOf(event.stage);
|
||||
const lastStage = activity?.steps.at(-1)?.stage;
|
||||
const lastRank = lastStage ? ASSISTANT_PROGRESS_STAGES.indexOf(lastStage) : -1;
|
||||
if (incomingRank < lastRank) return activities;
|
||||
|
||||
const steps = [...(activity?.steps ?? [])];
|
||||
const existingIndex = steps.findIndex((step) => step.stage === event.stage);
|
||||
const nextStep = { stage: event.stage, message: event.message };
|
||||
if (existingIndex >= 0) {
|
||||
steps[existingIndex] = nextStep;
|
||||
steps.splice(existingIndex + 1);
|
||||
} else {
|
||||
steps.push(nextStep);
|
||||
}
|
||||
return {
|
||||
...activities,
|
||||
[event.clientOperationId]: {
|
||||
workspaceId: event.workspaceId,
|
||||
directionId: event.directionId,
|
||||
status: 'active',
|
||||
turnId: activity?.turnId ?? null,
|
||||
steps,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function withCompletedAssistantActivity(
|
||||
activities: Record<string, DesignAssistantActivity>,
|
||||
pendingOperations: Record<string, PendingDesignOperation>,
|
||||
operation: PendingDesignOperation,
|
||||
workspace: DesignWorkspace,
|
||||
): Record<string, DesignAssistantActivity> {
|
||||
const turn = matchingChatTurn(
|
||||
workspace,
|
||||
operation,
|
||||
pendingOperations,
|
||||
activities[operation.id],
|
||||
);
|
||||
return turn
|
||||
? withAssistantActivityStatus(activities, operation.id, 'completed', turn.turnId)
|
||||
: withAssistantActivityStatus(activities, operation.id, 'completed');
|
||||
}
|
||||
|
||||
function parseWorkspaceEvent(event: Event): DesignWorkspaceEvent | null {
|
||||
const data = (event as MessageEvent<unknown>).data;
|
||||
if (typeof data !== 'string') return null;
|
||||
@@ -169,7 +328,11 @@ function parseWorkspaceEvent(event: Event): DesignWorkspaceEvent | null {
|
||||
if (typeof parsed.id !== 'string' || typeof parsed.type !== 'string') return null;
|
||||
if (![
|
||||
'design.session.snapshot',
|
||||
'design.assistant.progress',
|
||||
'design.assistant.delta',
|
||||
'command.completed',
|
||||
'command.failed',
|
||||
'command.cancelled',
|
||||
'design.direction.updated',
|
||||
'design.quote.blocked',
|
||||
'design.workspace.updated',
|
||||
@@ -275,10 +438,98 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
return workspace;
|
||||
};
|
||||
|
||||
const settleSuccessfulCommand = (
|
||||
operation: PendingDesignOperation,
|
||||
workspace: DesignWorkspace,
|
||||
): boolean => {
|
||||
let settled = false;
|
||||
set((state) => {
|
||||
const currentOperation = state.pendingOperations[operation.id];
|
||||
if (!currentOperation) return {};
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
const assistantStreams = { ...state.assistantStreams };
|
||||
|
||||
if (!isCurrentOperationSelection(state, currentOperation)) {
|
||||
delete pendingOperations[operation.id];
|
||||
delete assistantStreams[operation.id];
|
||||
settled = true;
|
||||
return {
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
),
|
||||
assistantStreams,
|
||||
};
|
||||
}
|
||||
|
||||
const canonicalWorkspace = state.workspace?.workspace.workspaceId
|
||||
=== workspace.workspace.workspaceId
|
||||
&& state.workspace.workspace.workspaceViewRevision
|
||||
> workspace.workspace.workspaceViewRevision
|
||||
? state.workspace
|
||||
: workspace;
|
||||
const chatMessage = currentOperation.command.kind === 'apply_input'
|
||||
&& currentOperation.command.input.kind === 'chat'
|
||||
? currentOperation.command.input.message
|
||||
: null;
|
||||
const canonicalTurn = chatMessage !== null
|
||||
? matchingChatTurn(
|
||||
canonicalWorkspace,
|
||||
currentOperation,
|
||||
state.pendingOperations,
|
||||
state.assistantActivities[operation.id],
|
||||
)
|
||||
: null;
|
||||
if (chatMessage !== null && !canonicalTurn) {
|
||||
return {
|
||||
...withWorkspace(state, canonicalWorkspace),
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
[operation.id]: { ...currentOperation, status: 'unknown', error: null },
|
||||
},
|
||||
assistantActivities: withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
'completed',
|
||||
),
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
delete pendingOperations[operation.id];
|
||||
delete assistantStreams[operation.id];
|
||||
const fieldDrafts = { ...state.fieldDrafts };
|
||||
for (const path of currentOperation.clearDraftPaths) delete fieldDrafts[path];
|
||||
const clearChatDraft = currentOperation.clearChatDraft
|
||||
&& (chatMessage === null || state.chatDraft.trim() === chatMessage);
|
||||
settled = true;
|
||||
return {
|
||||
...withWorkspace(state, canonicalWorkspace),
|
||||
pendingOperations,
|
||||
fieldDrafts,
|
||||
chatDraft: clearChatDraft ? '' : state.chatDraft,
|
||||
assistantActivities: chatMessage !== null
|
||||
? withCompletedAssistantActivity(
|
||||
state.assistantActivities,
|
||||
state.pendingOperations,
|
||||
currentOperation,
|
||||
canonicalWorkspace,
|
||||
)
|
||||
: withoutAssistantActivity(state.assistantActivities, operation.id),
|
||||
assistantStreams,
|
||||
};
|
||||
});
|
||||
if (settled) assistantStreamChunkIndexes.delete(operation.id);
|
||||
return settled;
|
||||
};
|
||||
|
||||
const executeCommand = async (
|
||||
command: DesignCommandInput,
|
||||
options: ExecuteCommandOptions,
|
||||
): Promise<DesignWorkspace> => {
|
||||
const existingOperation = get().pendingOperations[command.clientOperationId];
|
||||
const currentWorkspace = get().workspace;
|
||||
const operation: PendingDesignOperation = {
|
||||
id: command.clientOperationId,
|
||||
label: options.label,
|
||||
@@ -287,60 +538,138 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
error: null,
|
||||
clearDraftPaths: options.clearDraftPaths ?? [],
|
||||
clearChatDraft: options.clearChatDraft ?? false,
|
||||
baseRawTurnSequence: existingOperation?.baseRawTurnSequence
|
||||
?? (isChatCommand(command)
|
||||
&& currentWorkspace?.workspace.workspaceId === command.workspaceId
|
||||
? currentWorkspace.form.rawTurnSequence
|
||||
: null),
|
||||
workspaceSelectionGeneration,
|
||||
};
|
||||
set((state) => ({
|
||||
pendingOperations: { ...state.pendingOperations, [operation.id]: operation },
|
||||
error: null,
|
||||
quoteBlockers: command.kind === 'request_quote' ? [] : state.quoteBlockers,
|
||||
}));
|
||||
set((state) => {
|
||||
const existingActivity = state.assistantActivities[operation.id];
|
||||
const assistantActivities = isChatCommand(command)
|
||||
? existingActivity
|
||||
? {
|
||||
...state.assistantActivities,
|
||||
[operation.id]: {
|
||||
...existingActivity,
|
||||
status: 'active' as const,
|
||||
},
|
||||
}
|
||||
: state.assistantActivities
|
||||
: state.assistantActivities;
|
||||
return {
|
||||
pendingOperations: { ...state.pendingOperations, [operation.id]: operation },
|
||||
assistantActivities,
|
||||
error: null,
|
||||
quoteBlockers: command.kind === 'request_quote' ? [] : state.quoteBlockers,
|
||||
};
|
||||
});
|
||||
try {
|
||||
const result = await submitImageWorkspaceCommand(command);
|
||||
assistantStreamChunkIndexes.delete(operation.id);
|
||||
set((state) => {
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[operation.id];
|
||||
const fieldDrafts = { ...state.fieldDrafts };
|
||||
for (const path of operation.clearDraftPaths) delete fieldDrafts[path];
|
||||
const assistantStreams = { ...state.assistantStreams };
|
||||
delete assistantStreams[operation.id];
|
||||
return {
|
||||
...withWorkspace(state, result.workspace),
|
||||
pendingOperations,
|
||||
fieldDrafts,
|
||||
chatDraft: operation.clearChatDraft ? '' : state.chatDraft,
|
||||
assistantStreams,
|
||||
};
|
||||
});
|
||||
const observedTerminal = observedCommandTerminalEvents.get(operation.id);
|
||||
if (observedTerminal && observedTerminal.outcome !== 'succeeded') {
|
||||
throw terminalCommandError(observedTerminal);
|
||||
}
|
||||
observedCommandTerminalEvents.delete(operation.id);
|
||||
settleSuccessfulCommand(operation, result.workspace);
|
||||
return result.workspace;
|
||||
} catch (error) {
|
||||
const observedTerminal = observedCommandTerminalEvents.get(operation.id);
|
||||
if (observedTerminal) {
|
||||
if (observedTerminal.outcome === 'succeeded') {
|
||||
const refreshed = await fetchImageWorkspaceProject(command.workspaceId).catch(() => null);
|
||||
if (refreshed) {
|
||||
observedCommandTerminalEvents.delete(operation.id);
|
||||
settleSuccessfulCommand(operation, refreshed);
|
||||
return refreshed;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
observedCommandTerminalEvents.delete(operation.id);
|
||||
assistantStreamChunkIndexes.delete(operation.id);
|
||||
const terminalError = terminalCommandError(observedTerminal);
|
||||
set((state) => {
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[operation.id];
|
||||
const assistantStreams = { ...state.assistantStreams };
|
||||
delete assistantStreams[operation.id];
|
||||
return {
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
),
|
||||
assistantStreams,
|
||||
error: isCurrentOperationSelection(state, operation)
|
||||
? terminalError.message
|
||||
: state.error,
|
||||
};
|
||||
});
|
||||
throw terminalError;
|
||||
}
|
||||
|
||||
const message = messageForError(error);
|
||||
if (isUnknownCommandOutcome(error)) {
|
||||
set((state) => ({
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
[operation.id]: { ...operation, status: 'unknown', error: message },
|
||||
},
|
||||
...(isAuthError(error) ? { status: 'auth-required' as const } : {}),
|
||||
error: '操作结果尚未确认,可使用同一操作标识安全重试',
|
||||
}));
|
||||
set((state) => {
|
||||
const active = isCurrentOperationSelection(state, operation);
|
||||
return {
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
[operation.id]: { ...operation, status: 'unknown', error: message },
|
||||
},
|
||||
...(isAuthError(error) ? { status: 'auth-required' as const } : {}),
|
||||
assistantActivities: withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
'unknown',
|
||||
),
|
||||
error: active
|
||||
? '操作结果尚未确认,可使用同一操作标识安全重试'
|
||||
: state.error,
|
||||
};
|
||||
});
|
||||
} else if (isDirectionConflict(error)) {
|
||||
set((state) => {
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[operation.id];
|
||||
return { pendingOperations, error: '设计已在其他位置更新,已刷新最新内容,请再次提交' };
|
||||
return {
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
),
|
||||
error: isCurrentOperationSelection(state, operation)
|
||||
? '设计已在其他位置更新,已刷新最新内容,请再次提交'
|
||||
: state.error,
|
||||
};
|
||||
});
|
||||
await get().refreshWorkspace().catch(() => null);
|
||||
if (isCurrentOperationSelection(get(), operation)) {
|
||||
await get().refreshWorkspace().catch(() => null);
|
||||
}
|
||||
} else if (isAuthError(error)) {
|
||||
set((state) => {
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[operation.id];
|
||||
return { pendingOperations, status: 'auth-required', error: message };
|
||||
return {
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
),
|
||||
status: 'auth-required',
|
||||
error: message,
|
||||
};
|
||||
});
|
||||
} else if (isQuoteBlocked(error)) {
|
||||
set((state) => {
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[operation.id];
|
||||
return { pendingOperations, error: null };
|
||||
return {
|
||||
pendingOperations,
|
||||
error: isCurrentOperationSelection(state, operation) ? null : state.error,
|
||||
};
|
||||
});
|
||||
} else if (isDefinitiveCommandFailure(error)) {
|
||||
assistantStreamChunkIndexes.delete(operation.id);
|
||||
@@ -349,16 +678,33 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
delete pendingOperations[operation.id];
|
||||
const assistantStreams = { ...state.assistantStreams };
|
||||
delete assistantStreams[operation.id];
|
||||
return { pendingOperations, assistantStreams, error: message };
|
||||
return {
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
),
|
||||
assistantStreams,
|
||||
error: isCurrentOperationSelection(state, operation) ? message : state.error,
|
||||
};
|
||||
});
|
||||
if (error.status === 409) await get().refreshWorkspace().catch(() => null);
|
||||
if (error.status === 409 && isCurrentOperationSelection(get(), operation)) {
|
||||
await get().refreshWorkspace().catch(() => null);
|
||||
}
|
||||
} else {
|
||||
set((state) => ({
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
[operation.id]: { ...operation, status: 'unknown', error: message },
|
||||
},
|
||||
error: '操作结果尚未确认,可使用同一操作标识安全重试',
|
||||
assistantActivities: withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
operation.id,
|
||||
'unknown',
|
||||
),
|
||||
error: isCurrentOperationSelection(state, operation)
|
||||
? '操作结果尚未确认,可使用同一操作标识安全重试'
|
||||
: state.error,
|
||||
}));
|
||||
}
|
||||
throw error;
|
||||
@@ -376,6 +722,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
pendingOperations: {},
|
||||
fieldDrafts: {},
|
||||
chatDraft: '',
|
||||
assistantActivities: {},
|
||||
assistantStreams: {},
|
||||
quoteBlockers: [],
|
||||
error: null,
|
||||
@@ -396,6 +743,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
)
|
||||
? currentId
|
||||
: bootstrap.workspaces[0]?.workspaceId ?? null;
|
||||
if (selectedId !== currentId) workspaceSelectionGeneration += 1;
|
||||
set({
|
||||
status: 'ready',
|
||||
bootstrap,
|
||||
@@ -430,12 +778,14 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
createProject: async (title) => {
|
||||
const workspace = await createImageWorkspaceProject(title);
|
||||
selectionGeneration += 1;
|
||||
workspaceSelectionGeneration += 1;
|
||||
closeEventSource();
|
||||
assistantStreamChunkIndexes.clear();
|
||||
set((state) => ({
|
||||
...withWorkspace(state, workspace),
|
||||
fieldDrafts: {},
|
||||
chatDraft: '',
|
||||
assistantActivities: {},
|
||||
assistantStreams: {},
|
||||
quoteBlockers: [],
|
||||
lastEventId: null,
|
||||
@@ -473,6 +823,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
const deletingActive = get().activeWorkspaceId === workspaceId;
|
||||
if (deletingActive) {
|
||||
selectionGeneration += 1;
|
||||
workspaceSelectionGeneration += 1;
|
||||
closeEventSource();
|
||||
assistantStreamChunkIndexes.clear();
|
||||
}
|
||||
@@ -483,6 +834,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
deletingWorkspaceId: null,
|
||||
fieldDrafts: deletingActive ? {} : state.fieldDrafts,
|
||||
chatDraft: deletingActive ? '' : state.chatDraft,
|
||||
assistantActivities: deletingActive ? {} : state.assistantActivities,
|
||||
assistantStreams: deletingActive ? {} : state.assistantStreams,
|
||||
error: null,
|
||||
}));
|
||||
@@ -500,6 +852,8 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
selectProject: async (workspaceId) => {
|
||||
if (workspaceId === get().activeWorkspaceId && get().workspace) return;
|
||||
selectionGeneration += 1;
|
||||
workspaceSelectionGeneration += 1;
|
||||
const selectedWorkspaceGeneration = workspaceSelectionGeneration;
|
||||
closeEventSource();
|
||||
assistantStreamChunkIndexes.clear();
|
||||
set({
|
||||
@@ -509,6 +863,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
lastEventId: null,
|
||||
fieldDrafts: {},
|
||||
chatDraft: '',
|
||||
assistantActivities: {},
|
||||
assistantStreams: {},
|
||||
quoteBlockers: [],
|
||||
error: null,
|
||||
@@ -517,7 +872,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
await refreshWorkspaceById(workspaceId);
|
||||
get().connectEvents();
|
||||
} catch (error) {
|
||||
set({ error: messageForError(error), eventState: 'degraded' });
|
||||
if (get().activeWorkspaceId === workspaceId
|
||||
&& workspaceSelectionGeneration === selectedWorkspaceGeneration) {
|
||||
set({ error: messageForError(error), eventState: 'degraded' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -548,20 +906,128 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
return;
|
||||
}
|
||||
activeEventSource = source;
|
||||
source.onopen = () => set({ eventState: 'connected' });
|
||||
source.onopen = () => {
|
||||
if (connectionGeneration !== eventConnectionGeneration
|
||||
|| activeEventSource !== source) return;
|
||||
set({ eventState: 'connected' });
|
||||
};
|
||||
source.onerror = () => {
|
||||
if (connectionGeneration !== eventConnectionGeneration
|
||||
|| activeEventSource !== source) return;
|
||||
set({ eventState: 'degraded' });
|
||||
void get().refreshWorkspace().catch(() => null);
|
||||
};
|
||||
const receive = (raw: Event) => {
|
||||
if (connectionGeneration !== eventConnectionGeneration
|
||||
|| activeEventSource !== source) return;
|
||||
const event = parseWorkspaceEvent(raw);
|
||||
if (!event || get().activeWorkspaceId !== workspaceId) return;
|
||||
if (event.type === 'command.completed'
|
||||
|| event.type === 'command.failed'
|
||||
|| event.type === 'command.cancelled') {
|
||||
const pending = get().pendingOperations[event.clientOperationId];
|
||||
if (!pending || pending.command.workspaceId !== workspaceId) {
|
||||
set({ lastEventId: event.id });
|
||||
return;
|
||||
}
|
||||
observedCommandTerminalEvents.set(event.clientOperationId, event);
|
||||
if (event.outcome === 'succeeded') {
|
||||
set((state) => ({
|
||||
lastEventId: event.id,
|
||||
pendingOperations: {
|
||||
...state.pendingOperations,
|
||||
[event.clientOperationId]: {
|
||||
...pending,
|
||||
status: 'unknown',
|
||||
error: null,
|
||||
},
|
||||
},
|
||||
assistantActivities: withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
event.clientOperationId,
|
||||
'completed',
|
||||
),
|
||||
}));
|
||||
void get().refreshWorkspace().then((refreshed) => {
|
||||
if (refreshed) settleSuccessfulCommand(pending, refreshed);
|
||||
}).catch(() => undefined).finally(() => {
|
||||
if (pending.status === 'unknown') {
|
||||
observedCommandTerminalEvents.delete(event.clientOperationId);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
assistantStreamChunkIndexes.delete(event.clientOperationId);
|
||||
set((state) => {
|
||||
const operation = state.pendingOperations[event.clientOperationId];
|
||||
if (!operation || operation.command.workspaceId !== workspaceId) {
|
||||
return { lastEventId: event.id };
|
||||
}
|
||||
const pendingOperations = { ...state.pendingOperations };
|
||||
delete pendingOperations[event.clientOperationId];
|
||||
const assistantStreams = { ...state.assistantStreams };
|
||||
delete assistantStreams[event.clientOperationId];
|
||||
return {
|
||||
lastEventId: event.id,
|
||||
pendingOperations,
|
||||
assistantActivities: withoutAssistantActivity(
|
||||
state.assistantActivities,
|
||||
event.clientOperationId,
|
||||
),
|
||||
assistantStreams,
|
||||
error: isCurrentOperationSelection(state, operation)
|
||||
? terminalCommandError(event).message
|
||||
: state.error,
|
||||
};
|
||||
});
|
||||
if (pending.status === 'unknown') {
|
||||
observedCommandTerminalEvents.delete(event.clientOperationId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.type === 'design.assistant.progress') {
|
||||
set((state) => {
|
||||
const pending = state.pendingOperations[event.clientOperationId];
|
||||
if (!state.workspace
|
||||
|| pending?.command.kind !== 'apply_input'
|
||||
|| pending.command.input.kind !== 'chat'
|
||||
|| pending.command.workspaceId !== workspaceId
|
||||
|| event.workspaceId !== workspaceId
|
||||
|| event.directionId !== state.workspace.form.directionId) {
|
||||
return { lastEventId: event.id };
|
||||
}
|
||||
return {
|
||||
lastEventId: event.id,
|
||||
assistantActivities: withAssistantProgress(state.assistantActivities, event),
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.type === 'design.assistant.delta') {
|
||||
const state = get();
|
||||
const pending = state.pendingOperations[event.clientOperationId];
|
||||
if (!state.workspace
|
||||
|| pending?.command.kind !== 'apply_input'
|
||||
|| pending.command.input.kind !== 'chat'
|
||||
|| pending.command.workspaceId !== workspaceId
|
||||
|| event.workspaceId !== workspaceId
|
||||
|| event.directionId !== state.workspace.form.directionId) {
|
||||
set({ lastEventId: event.id });
|
||||
return;
|
||||
}
|
||||
const previousChunkIndex = assistantStreamChunkIndexes.get(event.clientOperationId);
|
||||
if (previousChunkIndex !== undefined && event.chunkIndex <= previousChunkIndex) return;
|
||||
if (previousChunkIndex !== undefined && event.chunkIndex <= previousChunkIndex) {
|
||||
set({ lastEventId: event.id });
|
||||
return;
|
||||
}
|
||||
assistantStreamChunkIndexes.set(event.clientOperationId, event.chunkIndex);
|
||||
set((state) => ({
|
||||
lastEventId: event.id,
|
||||
assistantActivities: withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
event.clientOperationId,
|
||||
'completed',
|
||||
),
|
||||
assistantStreams: {
|
||||
...state.assistantStreams,
|
||||
[event.clientOperationId]: `${state.assistantStreams[event.clientOperationId] ?? ''}${event.delta}`,
|
||||
@@ -569,6 +1035,19 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (event.type === 'design.direction.updated') {
|
||||
const pending = get().pendingOperations[event.clientOperationId];
|
||||
if (pending?.status === 'submitting') {
|
||||
observedCommandTerminalEvents.set(event.clientOperationId, {
|
||||
id: event.id,
|
||||
type: 'command.completed',
|
||||
workspaceId: event.form.workspaceId,
|
||||
clientOperationId: event.clientOperationId,
|
||||
outcome: 'succeeded',
|
||||
errorCode: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
set((state) => {
|
||||
if (!state.workspace) return { lastEventId: event.id };
|
||||
const nextWorkspace = applyEventToWorkspace(state.workspace, event);
|
||||
@@ -578,10 +1057,20 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
if (event.type === 'design.quote.blocked') {
|
||||
delete pendingOperations[event.clientOperationId];
|
||||
}
|
||||
const assistantActivities = event.type === 'design.direction.updated'
|
||||
&& event.operation.turnId
|
||||
? withAssistantActivityStatus(
|
||||
state.assistantActivities,
|
||||
event.clientOperationId,
|
||||
'completed',
|
||||
event.operation.turnId,
|
||||
)
|
||||
: state.assistantActivities;
|
||||
updateBackgroundLease(nextWorkspace.tasks);
|
||||
return {
|
||||
workspace: nextWorkspace,
|
||||
pendingOperations,
|
||||
assistantActivities,
|
||||
bootstrap: state.bootstrap
|
||||
? {
|
||||
...state.bootstrap,
|
||||
@@ -597,13 +1086,22 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
error: event.type === 'design.quote.blocked' ? null : state.error,
|
||||
};
|
||||
});
|
||||
if (event.type === 'design.workspace.updated' && event.changedDirection) {
|
||||
if (event.type === 'design.direction.updated') {
|
||||
const pending = get().pendingOperations[event.clientOperationId];
|
||||
void get().refreshWorkspace().then((refreshed) => {
|
||||
if (pending && refreshed) settleSuccessfulCommand(pending, refreshed);
|
||||
}).catch(() => null);
|
||||
} else if (event.type === 'design.workspace.updated' && event.changedDirection) {
|
||||
void get().refreshWorkspace().catch(() => null);
|
||||
}
|
||||
};
|
||||
for (const eventType of [
|
||||
'design.session.snapshot',
|
||||
'design.assistant.progress',
|
||||
'design.assistant.delta',
|
||||
'command.completed',
|
||||
'command.failed',
|
||||
'command.cancelled',
|
||||
'design.direction.updated',
|
||||
'design.quote.blocked',
|
||||
'design.workspace.updated',
|
||||
@@ -726,8 +1224,10 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
|
||||
reset: () => {
|
||||
selectionGeneration += 1;
|
||||
workspaceSelectionGeneration += 1;
|
||||
closeEventSource();
|
||||
assistantStreamChunkIndexes.clear();
|
||||
observedCommandTerminalEvents.clear();
|
||||
updateBackgroundLease([]);
|
||||
set({
|
||||
status: 'idle',
|
||||
@@ -740,6 +1240,7 @@ export const useImageWorkspaceStore = create<ImageWorkspaceState>((set, get) =>
|
||||
pendingOperations: {},
|
||||
fieldDrafts: {},
|
||||
chatDraft: '',
|
||||
assistantActivities: {},
|
||||
assistantStreams: {},
|
||||
quoteBlockers: [],
|
||||
error: null,
|
||||
|
||||
Reference in New Issue
Block a user