FN-6710: use Working for chat wait states

Chat now labels pre-stream assistant activity as work in progress instead of a connection step.

- Switch full Chat and Quick Chat waiting copy to the chat.workingStatus translation key.
- Rename localized chat loading text from Connecting to Working across shipped locales and generated resource types.
- Update chat recovery docs, implementation comments, and tests to expect the Working indicator.

Files changed:
 docs/dashboard-guide.md                                        |  2 +-
 packages/dashboard/app/components/ChatView.tsx                 |  6 +++++-
 packages/dashboard/app/components/QuickChatFAB.tsx             |  2 +-
 packages/dashboard/app/components/__tests__/ChatView.test.tsx  | 10 +++++-----
 .../dashboard/app/components/__tests__/QuickChatFAB.test.tsx   |  4 ++--
 packages/dashboard/app/hooks/useChat.ts                        |  4 ++--
 packages/dashboard/app/hooks/useQuickChat.ts                   |  2 +-
 packages/i18n/locales/en/app.json                              |  2 +-
 packages/i18n/locales/es/app.json                              |  2 +-
 packages/i18n/locales/fr/app.json                              |  2 +-
 packages/i18n/locales/ko/app.json                              |  2 +-
 packages/i18n/locales/zh-CN/app.json                           |  2 +-
 packages/i18n/locales/zh-TW/app.json                           |  2 +-
 packages/i18n/src/resources.d.ts                               |  2 +-
 14 files changed, 24 insertions(+), 20 deletions(-)

Fusion-Task-Id: FN-6710

Fusion-Task-Lineage: a7592c7d-605f-4a9b-b0d3-be875ed2eff5
This commit is contained in:
gsxdsm
2026-06-19 06:41:28 -07:00
parent cfddde5a45
commit 3df9107e58
14 changed files with 24 additions and 20 deletions

View File

@@ -238,7 +238,7 @@ Chat view provides project-scoped conversations with agents.
- Entering `/new` or `/clear` (exact match after trimming) in the composer starts a fresh thread for the current chat target instead of sending the literal command to the model
- On mobile, the New Chat and Delete Conversation dialogs use a compact inset treatment (centered, viewport-bounded, internally scrollable) instead of the app's default full-height mobile modal chrome.
- Full Chat and Quick Chat both consume the same streamed `/api/chat/sessions/:id/messages` response contract, and both now prefer the authoritative assistant `message` snapshot on `done` while still accumulating `text` chunks when present (so providers without incremental text streaming still render output immediately)
- In-progress assistant responses now survive refresh/navigation while generation is still active: Chat restores the last durable in-flight text/thinking/tool state immediately, keeps the prior persisted conversation visible, then resumes streaming from the stored replay point; any new text, thinking, or tool-call updates append to that restored bubble instead of replacing it or starting from an empty "Connecting…" placeholder.
- In-progress assistant responses now survive refresh/navigation while generation is still active: Chat restores the last durable in-flight text/thinking/tool state immediately, keeps the prior persisted conversation visible, then resumes streaming from the stored replay point; any new text, thinking, or tool-call updates append to that restored bubble instead of replacing it or starting from an empty "Working…" placeholder.
- If a regular Chat stream drops with a hidden-tab/browser-suspension error (for example `Load failed`) while the server is still generating, Chat suppresses the false error banner, re-attaches to the in-progress stream using the durable replay state, and reconciles the final assistant reply when generation completes.
- If you queue a follow-up user message while the assistant is still streaming, Chat now persists that queued text per session so leaving and returning to the view still restores and sends it once the active response finishes.
- Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail.

View File

@@ -2892,7 +2892,11 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
renderAssistantContent(streamingText, showAllAsPlain)
) : (
<div className="chat-message-content chat-message-content--waiting">
{streamingThinking ? t("chat.thinkingStatus", "Thinking…") : t("chat.connectingStatus", "Connecting…")}
{/*
FNXC:ChatLoadingCopy 2026-06-19-06:13:
The post-send waiting indicator reads "Working…" when no streamed text or thinking content has arrived yet, because the UI is waiting on work rather than a transport connection.
*/}
{streamingThinking ? t("chat.thinkingStatus", "Thinking…") : t("chat.workingStatus", "Working…")}
</div>
)}
{showProviderResponseCopy && streamingText && renderCopyAction("__streaming__", streamingText, "chat-copy-response-streaming")}

View File

@@ -3231,7 +3231,7 @@ export function QuickChatFAB({
</>
) : (
<p className="quick-chat-panel-waiting" data-testid="quick-chat-waiting">
{streamingThinking ? t("chat.thinkingStatus", "Thinking…") : t("chat.connectingStatus", "Connecting…")}
{streamingThinking ? t("chat.thinkingStatus", "Thinking…") : t("chat.workingStatus", "Working…")}
</p>
)}
{renderToolCalls(streamingToolCalls, true, t, {

View File

@@ -2469,10 +2469,10 @@ describe("ChatView", () => {
const { rerender } = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(document.querySelector(".chat-message--streaming")?.textContent).toContain("Connecting");
expect(document.querySelector(".chat-message--streaming")?.textContent).toContain("Working");
rerender(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(document.querySelector(".chat-message--streaming")?.textContent).toContain("Connecting");
expect(document.querySelector(".chat-message--streaming")?.textContent).toContain("Working");
expect(screen.queryByText("Start a new conversation")).not.toBeInTheDocument();
expect(screen.queryByText("No messages yet. Start the conversation!")).not.toBeInTheDocument();
expect(screen.getByTestId("chat-back-btn")).toBeInTheDocument();
@@ -2494,7 +2494,7 @@ describe("ChatView", () => {
const streamingMessage = document.querySelector(".chat-message--streaming") as HTMLElement | null;
expect(streamingMessage).toBeInTheDocument();
expect(streamingMessage?.textContent).toContain("Connecting");
expect(streamingMessage?.textContent).toContain("Working");
expect(screen.queryByText("Loading messages...")).not.toBeInTheDocument();
});
@@ -2511,10 +2511,10 @@ describe("ChatView", () => {
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// Streaming message should show with "Connecting..." text
// Streaming message should show with "Working..." text
const streamingMessage = document.querySelector(".chat-message--streaming") as HTMLElement | null;
expect(streamingMessage).toBeInTheDocument();
expect(streamingMessage?.textContent).toContain("Connecting");
expect(streamingMessage?.textContent).toContain("Working");
// Waiting class should be present
const waitingContent = streamingMessage?.querySelector(".chat-message-content--waiting");

View File

@@ -1685,7 +1685,7 @@ describe("QuickChatFAB session-first UX", () => {
fireEvent.click(screen.getByTestId("quick-chat-send"));
expect(await screen.findByTestId("quick-chat-streaming-message")).toBeInTheDocument();
expect(screen.getByTestId("quick-chat-waiting")).toHaveTextContent("Connecting…");
expect(screen.getByTestId("quick-chat-waiting")).toHaveTextContent("Working…");
expect(mockStreamChatResponse).toHaveBeenCalledTimes(2);
});
@@ -1747,7 +1747,7 @@ describe("QuickChatFAB session-first UX", () => {
fireEvent.click(screen.getByTestId("quick-chat-send"));
expect(await screen.findByTestId("quick-chat-streaming-message")).toBeInTheDocument();
expect(screen.getByTestId("quick-chat-waiting")).toHaveTextContent("Connecting…");
expect(screen.getByTestId("quick-chat-waiting")).toHaveTextContent("Working…");
expect(screen.queryByText("Loading conversation…")).not.toBeInTheDocument();
});

View File

@@ -684,8 +684,8 @@ export function useChat(
// Recover streaming state if the server reports an active generation.
// After a reload/HMR, the server keeps generating but the UI loses
// all streaming state. Showing "Connecting…" immediately tells the
// user the AI is still working.
// all streaming state. Showing "Working…" immediately tells the
// user the AI is still processing the request.
if (session?.isGenerating) {
attachIfGenerating(session.id, session.inFlightGeneration, { priorThreadLoadAlreadyStarted: true });
}

View File

@@ -457,7 +457,7 @@ export function useQuickChat(
// Recover streaming state if server is still generating for this session.
// After a reload/HMR, the server keeps generating but the UI loses
// all streaming state. Show the "Connecting…" indicator immediately.
// all streaming state. Show the "Working…" indicator immediately.
if (existingSession.isGenerating) {
attachIfGenerating(existingSession.id, existingSession.inFlightGeneration);
}

View File

@@ -1195,7 +1195,7 @@
"cancelButton": "Cancel",
"clearConversationFailed": "Failed to clear conversation",
"closeQuickChat": "Close quick chat",
"connectingStatus": "Connecting…",
"workingStatus": "Working…",
"conversationArchived": "Conversation archived",
"conversationDeleted": "Conversation deleted",
"copyFailed": "Copy failed",

View File

@@ -1194,7 +1194,7 @@
"cancelButton": "Cancelar",
"clearConversationFailed": "Error al borrar la conversación",
"closeQuickChat": "Cerrar chat rápido",
"connectingStatus": "Conectando…",
"workingStatus": "Trabajando…",
"conversationArchived": "Conversación archivada",
"conversationDeleted": "Conversación eliminada",
"copyFailed": "Error al copiar",

View File

@@ -1194,7 +1194,7 @@
"cancelButton": "Annuler",
"clearConversationFailed": "Échec de l'effacement de la conversation",
"closeQuickChat": "Fermer le chat rapide",
"connectingStatus": "Connexion en cours…",
"workingStatus": "Traitement en cours…",
"conversationArchived": "Conversation archivée",
"conversationDeleted": "Conversation supprimée",
"copyFailed": "Échec de la copie",

View File

@@ -1194,7 +1194,7 @@
"cancelButton": "취소",
"clearConversationFailed": "대화 초기화 실패",
"closeQuickChat": "빠른 채팅 닫기",
"connectingStatus": "연결 중…",
"workingStatus": "작업 중…",
"conversationArchived": "대화가 보관되었습니다",
"conversationDeleted": "대화가 삭제되었습니다",
"copyFailed": "복사 실패",

View File

@@ -1194,7 +1194,7 @@
"cancelButton": "取消",
"clearConversationFailed": "清除对话失败",
"closeQuickChat": "关闭快速聊天",
"connectingStatus": "连接中……",
"workingStatus": "处理中……",
"conversationArchived": "对话已归档",
"conversationDeleted": "对话已删除",
"copyFailed": "复制失败",

View File

@@ -1194,7 +1194,7 @@
"cancelButton": "取消",
"clearConversationFailed": "清除對話失敗",
"closeQuickChat": "關閉快速聊天",
"connectingStatus": "連接中……",
"workingStatus": "處理中……",
"conversationArchived": "對話已封存",
"conversationDeleted": "對話已刪除",
"copyFailed": "複製失敗",

View File

@@ -1197,7 +1197,7 @@ export default interface Resources {
"cancelButton": "Cancel",
"clearConversationFailed": "Failed to clear conversation",
"closeQuickChat": "Close quick chat",
"connectingStatus": "Connecting…",
"workingStatus": "Working…",
"conversationArchived": "Conversation archived",
"conversationDeleted": "Conversation deleted",
"copyFailed": "Copy failed",