fix(chat): reconcile reply recovery with continuous agent conversations
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
鲨鱼辣椒
2026-09-28 18:14:12 +08:00
parent 44b16f8fbd
commit cafebca057
13 changed files with 159 additions and 34 deletions

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { ArrowUp, ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
import { useAuthStore } from '@/stores/auth';
import { teacherApi, teacherTopicsPath, legacyTopicBase, type TeacherCatalogState } from '@/lib/coding-teacher';
@@ -101,9 +101,7 @@ export function TeacherChatPanel({
const focusedOnce = useRef(false);
const [topics, setTopics] = useState<TeacherTopicList>({ items: [], lastSelectedTopicId: null });
const [storedTopic, setTopic] = useState<TeacherTopic | null>(cachedView?.topic ?? null);
const topic = useMemo(() => storedTopic && continuous ? { ...storedTopic,
discussion: storedTopic.projectId === projectId ? storedTopic.discussion : storedTopic.conversation?.discussions[projectId] }
: storedTopic, [storedTopic, continuous, projectId]);
const topic = storedTopic;
const legacyFriend = topic?.role === 'friend';
const legacyView = Boolean(continuous && topic && !topic.conversation);
const viewingLegacy = useRef(false);
@@ -383,15 +381,18 @@ export function TeacherChatPanel({
if (version === generation.current) setBusy(false);
}
};
const retryScopeMatches = (request: TeacherRequest) => !continuous
|| (request.projectId === projectId && (request.sourceConversationId ?? '') === sourceId);
const send = async (action?: Pick<TeacherSend, 'text' | 'intent'>, retry?: TeacherRequest) => {
// A historical retry stays with its original topic and published Agent.
if (retry && (topicRef.current?.id !== topic?.id || topic?.projectId !== projectId
if (retry && (topicRef.current?.id !== topic?.id || !topic || !retryScopeMatches(retry)
|| (!continuous && topic.projectId !== projectId)
|| (selectedAgentId && topic.definition.config_id !== selectedAgentId)
|| !topic.requests.some((request) => request === retry && request.status === 'completed' && request.replyIncomplete))) return;
const question = action?.text ?? text;
const questionReferences = retry?.references ?? (action ? [] : references);
const questionSourceId = retry
? retry.sourceConversationId ?? (topic?.sourceConversationId === 'project' ? undefined : topic?.sourceConversationId)
? retry.sourceConversationId ?? (continuous || topic?.sourceConversationId === 'project' ? undefined : topic?.sourceConversationId)
: sourceId;
if (!question.trim() || busy || sendInFlight.current || !topicEnabled || topicRef.current?.requests.some((request) => ['preparing', 'running'].includes(request.status))) return;
if (continuous && !retry && questionReferences.length && (referenceScope.projectId !== projectId || referenceScope.sourceId !== sourceId)) {
@@ -423,8 +424,8 @@ export function TeacherChatPanel({
// Copy changes must not create another charge for an uncertain hint request.
const retrySuggestions = !retry && action?.intent === 'suggestions' && previous?.intent === 'suggestions';
const input =
previous && (!continuous || previous.projectId === projectId)
&& previous.sourceConversationId === (questionSourceId || undefined) && (retrySuggestions || (
previous && (!continuous || (previous.projectId === projectId
&& previous.sourceConversationId === (questionSourceId || undefined))) && (retrySuggestions || (
previous.text === question &&
(previous.intent ?? 'question') === (action?.intent ?? 'question') &&
JSON.stringify(previous.references) === JSON.stringify(questionReferences) &&
@@ -523,7 +524,8 @@ export function TeacherChatPanel({
</details>}
{request.status === 'completed' && request.replyIncomplete ? <div className="space-y-3">
<p role="status" className="text-[13px] leading-7 text-muted-foreground">这次回复未能完整显示</p>
{request.intent !== 'check-in' && request.text.trim() && <button type="button" disabled={helpUnavailable} onClick={() => void send({ text: request.text, intent: request.intent }, request)} className="consultation-retry">重新回答</button>}
{request.intent !== 'check-in' && request.text.trim() && <button type="button" disabled={helpUnavailable || !retryScopeMatches(request)} onClick={() => void send({ text: request.text, intent: request.intent }, request)} className="consultation-retry">重新回答</button>}
{!retryScopeMatches(request) && <p className="text-xs text-muted-foreground">回到原项目和操作对话后,可以重新回答。</p>}
</div> : request.intent === 'suggestions' ? <>
{['running', 'preparing'].includes(request.status)
? <p role="status" className="flex items-center gap-2 text-[13px] leading-7 text-muted-foreground"><Loader2 className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />我看看你最近做到了哪里…</p>