mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix(desktop): keep turn progress feedback visible
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
buildActivitySegments,
|
||||
countFailedToolCalls,
|
||||
hasUnresolvedToolCalls,
|
||||
THINKING_SEGMENT_KEY,
|
||||
toolCallDurationMs,
|
||||
type ActivityStep,
|
||||
} from './activityGroupModel'
|
||||
@@ -51,7 +52,13 @@ export const ActivityGroup = memo(function ActivityGroup({
|
||||
const toolCalls = useMemo(() => activityStepToolCalls(steps), [steps])
|
||||
const segments = useMemo(() => buildActivitySegments(steps, t), [steps, t])
|
||||
const failedCount = countFailedToolCalls(toolCalls, resultMap, childToolCallsByParent)
|
||||
const isRunning = Boolean(isStreaming) || hasUnresolvedToolCalls(toolCalls, resultMap, childToolCallsByParent)
|
||||
const hasActiveThinking = Boolean(activeThinkingId) && steps.some(
|
||||
(step) => step.kind === 'thinking' && step.message.id === activeThinkingId,
|
||||
)
|
||||
const isRunning =
|
||||
Boolean(isStreaming) ||
|
||||
hasActiveThinking ||
|
||||
hasUnresolvedToolCalls(toolCalls, resultMap, childToolCallsByParent)
|
||||
|
||||
const soleToolCall = steps.length === 1 && steps[0]?.kind === 'tool' ? steps[0].toolCall : null
|
||||
if (soleToolCall) {
|
||||
@@ -101,7 +108,12 @@ export const ActivityGroup = memo(function ActivityGroup({
|
||||
<span aria-hidden="true" className="shrink-0 text-[var(--color-text-tertiary)]">·</span>
|
||||
)}
|
||||
<span className="flex shrink-0 items-center gap-[6px] whitespace-nowrap">
|
||||
<segment.icon size={13} strokeWidth={1.8} aria-hidden="true" />
|
||||
<segment.icon
|
||||
size={13}
|
||||
strokeWidth={1.8}
|
||||
className={hasActiveThinking && segment.key === THINKING_SEGMENT_KEY ? 'animate-pulse' : undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{segment.label}
|
||||
</span>
|
||||
</Fragment>
|
||||
|
||||
@@ -1115,7 +1115,10 @@ describe('ChatInput file mentions', () => {
|
||||
})
|
||||
|
||||
it('starts an empty active session on the selected branch without an isolated worktree', async () => {
|
||||
mocks.create.mockResolvedValueOnce({ sessionId: 'created-direct', workDir: '/repo' })
|
||||
let resolveCreate!: (value: { sessionId: string; workDir: string }) => void
|
||||
mocks.create.mockReturnValueOnce(new Promise((resolve) => {
|
||||
resolveCreate = resolve
|
||||
}))
|
||||
useSessionStore.setState({
|
||||
sessions: [{
|
||||
id: sessionId,
|
||||
@@ -1166,12 +1169,23 @@ describe('ChatInput file mentions', () => {
|
||||
repository: { branch: 'feature/a', worktree: false },
|
||||
})
|
||||
})
|
||||
expect(mocks.delete).toHaveBeenCalledWith(sessionId)
|
||||
expect(mocks.wsSend).toHaveBeenCalledWith('created-direct', {
|
||||
type: 'user_message',
|
||||
content: 'run on feature branch',
|
||||
attachments: [],
|
||||
|
||||
// Session creation can take seconds. The click must cross the store
|
||||
// boundary immediately so the transcript can replace its empty hero with
|
||||
// a live placeholder before the new session id exists.
|
||||
expect(useChatStore.getState().sessions[sessionId]?.isPreparingTurn).toBe(true)
|
||||
|
||||
resolveCreate({ sessionId: 'created-direct', workDir: '/repo' })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.wsSend).toHaveBeenCalledWith('created-direct', {
|
||||
type: 'user_message',
|
||||
content: 'run on feature branch',
|
||||
attachments: [],
|
||||
})
|
||||
})
|
||||
expect(useChatStore.getState().sessions['created-direct']?.isPreparingTurn).toBe(false)
|
||||
expect(mocks.delete).toHaveBeenCalledWith(sessionId)
|
||||
})
|
||||
|
||||
it('preserves explicit permission mode when replacing an empty session for branch launch', async () => {
|
||||
|
||||
@@ -185,6 +185,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
updateQueuedUserMessage,
|
||||
removeQueuedUserMessage,
|
||||
sendQueuedUserMessage,
|
||||
setPreparingTurn,
|
||||
} = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
||||
@@ -193,6 +194,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
const launchBranch = repositoryLaunchDraft?.branch ?? null
|
||||
const launchUseWorktree = repositoryLaunchDraft?.useWorktree ?? false
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const isPreparingTurn = Boolean(sessionState?.isPreparingTurn)
|
||||
const slashCommands = sessionState?.slashCommands ?? []
|
||||
const composerPrefill = sessionState?.composerPrefill ?? null
|
||||
const composerInsertion = sessionState?.composerInsertion ?? null
|
||||
@@ -295,6 +297,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
: null
|
||||
const canSubmit = !isWorkspaceMissing &&
|
||||
!launchTransitioning &&
|
||||
!isPreparingTurn &&
|
||||
(!showLaunchControls || launchReady || !!pendingSlashUiAction) &&
|
||||
(input.trim().length > 0 || (!isMemberSession && (attachments.length > 0 || hasWorkspaceReferences)))
|
||||
const composerAttachments = useMemo(
|
||||
@@ -790,14 +793,23 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
(gitInfo?.branch ? launchBranch !== gitInfo.branch : true)
|
||||
if (shouldReplaceForRepositoryLaunch) {
|
||||
setLaunchTransitioning(true)
|
||||
const placeholderSessionId = targetSessionId
|
||||
setPreparingTurn(placeholderSessionId, true)
|
||||
try {
|
||||
const newSessionId = await replaceEmptySession(activeLaunchWorkDir, {
|
||||
branch: launchBranch,
|
||||
worktree: launchUseWorktree,
|
||||
})
|
||||
if (!newSessionId) return
|
||||
if (!newSessionId) {
|
||||
setPreparingTurn(placeholderSessionId, false)
|
||||
return
|
||||
}
|
||||
targetSessionId = newSessionId
|
||||
// Keep the placeholder continuous across the old-id -> new-id swap.
|
||||
// sendMessage clears it in the same turn that appends the user row.
|
||||
setPreparingTurn(targetSessionId, true)
|
||||
} catch (error) {
|
||||
setPreparingTurn(placeholderSessionId, false)
|
||||
useUIStore.getState().addToast({
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : t('empty.failedToCreate'),
|
||||
@@ -1291,7 +1303,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
onCompositionStart={() => { composingRef.current = true }}
|
||||
onCompositionEnd={() => { composingRef.current = false }}
|
||||
placeholder={composerPlaceholder}
|
||||
disabled={isWorkspaceMissing}
|
||||
disabled={isWorkspaceMissing || launchTransitioning || isPreparingTurn}
|
||||
className="flex-1"
|
||||
editorClassName="max-h-[200px] overflow-y-auto py-2 leading-relaxed text-[var(--color-text-primary)]"
|
||||
aria={{
|
||||
@@ -1317,7 +1329,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
onCompositionStart={() => { composingRef.current = true }}
|
||||
onCompositionEnd={() => { composingRef.current = false }}
|
||||
placeholder={composerPlaceholder}
|
||||
disabled={isWorkspaceMissing}
|
||||
disabled={isWorkspaceMissing || launchTransitioning || isPreparingTurn}
|
||||
editorClassName={`max-h-[200px] overflow-y-auto text-sm leading-relaxed text-[var(--color-text-primary)] ${
|
||||
useCompactChrome ? 'py-1.5' : 'py-2'
|
||||
}`}
|
||||
|
||||
@@ -1648,6 +1648,18 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(screen.getByText(/waiting \d+s/)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows a thinking placeholder as soon as first-turn preparation begins', () => {
|
||||
render(<MessageList />)
|
||||
|
||||
act(() => {
|
||||
useChatStore.getState().setPreparingTurn(ACTIVE_TAB, true)
|
||||
})
|
||||
|
||||
const status = screen.getByTestId('turn-status-indicator')
|
||||
expect(status.getAttribute('role')).toBe('status')
|
||||
expect(status.textContent).toContain('Thinking')
|
||||
})
|
||||
|
||||
it('shows the non-streaming fallback notice in the active turn indicator', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
@@ -2022,6 +2034,48 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(group.toolCalls.map((toolCall) => toolCall.toolUseId)).toEqual(['read-1', 'bash-1'])
|
||||
})
|
||||
|
||||
it('keeps a completed tool group visibly live while post-tool thinking streams', () => {
|
||||
render(<MessageList sessionId={ACTIVE_TAB} />)
|
||||
|
||||
const store = useChatStore.getState()
|
||||
act(() => {
|
||||
store.handleServerMessage(ACTIVE_TAB, {
|
||||
type: 'content_start',
|
||||
blockType: 'tool_use',
|
||||
toolName: 'Bash',
|
||||
toolUseId: 'bash-live-1',
|
||||
})
|
||||
store.handleServerMessage(ACTIVE_TAB, {
|
||||
type: 'tool_use_complete',
|
||||
toolName: 'Bash',
|
||||
toolUseId: 'bash-live-1',
|
||||
input: { command: 'git status --short' },
|
||||
})
|
||||
store.handleServerMessage(ACTIVE_TAB, {
|
||||
type: 'tool_result',
|
||||
toolUseId: 'bash-live-1',
|
||||
content: '',
|
||||
isError: false,
|
||||
})
|
||||
store.handleServerMessage(ACTIVE_TAB, {
|
||||
type: 'thinking',
|
||||
text: 'Now inspect the remaining UI paths.',
|
||||
})
|
||||
})
|
||||
|
||||
const group = screen.getByTestId('activity-group')
|
||||
expect(group.getAttribute('data-running')).toBe('true')
|
||||
expect(group.querySelector('.animate-spin')).not.toBeNull()
|
||||
expect(group.querySelector('.animate-pulse')).not.toBeNull()
|
||||
|
||||
act(() => {
|
||||
store.handleServerMessage(ACTIVE_TAB, { type: 'status', state: 'idle' })
|
||||
})
|
||||
|
||||
expect(group.getAttribute('data-running')).toBe('false')
|
||||
expect(group.querySelector('.animate-spin')).toBeNull()
|
||||
})
|
||||
|
||||
it('leaves a run of pure reasoning as standalone thinking blocks', () => {
|
||||
const messages: UIMessage[] = [
|
||||
{ id: 'think-1', type: 'thinking', content: 'First consider the call sites.', timestamp: 1 },
|
||||
|
||||
@@ -1690,6 +1690,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false }
|
||||
const addToast = useUIStore((s) => s.addToast)
|
||||
const messages = sessionState?.messages ?? EMPTY_MESSAGES
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const isPreparingTurn = Boolean(sessionState?.isPreparingTurn)
|
||||
const historyMutationEpoch = sessionState?.historyMutationEpoch ?? 0
|
||||
const streamingText = sessionState?.streamingText ?? ''
|
||||
const streamingToolInput = sessionState?.streamingToolInput ?? ''
|
||||
@@ -1714,6 +1715,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false }
|
||||
(permission) => permission.toolName !== 'AskUserQuestion',
|
||||
)
|
||||
const shouldFollowContentResize =
|
||||
isPreparingTurn ||
|
||||
streamingText.trim().length > 0 ||
|
||||
chatState === 'streaming' ||
|
||||
chatState === 'compacting' ||
|
||||
@@ -1774,6 +1776,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false }
|
||||
const [messageListWidth, setMessageListWidth] = useState<number | null>(null)
|
||||
const branchActionsDisabled =
|
||||
isMemberSession ||
|
||||
isPreparingTurn ||
|
||||
chatState !== 'idle' ||
|
||||
hasRunningBackgroundTasks ||
|
||||
streamingText.trim().length > 0 ||
|
||||
@@ -2937,6 +2940,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false }
|
||||
describing — it is part of the conversation, not composer chrome. */}
|
||||
{(hasApiRetry ||
|
||||
hasStreamingFallback ||
|
||||
isPreparingTurn ||
|
||||
chatState === 'tool_executing' ||
|
||||
(chatState === 'thinking' && !activeThinkingId)) && (
|
||||
<StreamingIndicator />
|
||||
|
||||
@@ -50,7 +50,9 @@ export function StreamingIndicator() {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const chatState = sessionState?.isPreparingTurn
|
||||
? 'thinking'
|
||||
: sessionState?.chatState ?? 'idle'
|
||||
const statusVerb = sessionState?.statusVerb ?? ''
|
||||
const apiRetry = sessionState?.apiRetry ?? null
|
||||
const streamingFallback = sessionState?.streamingFallback ?? null
|
||||
@@ -136,7 +138,12 @@ export function StreamingIndicator() {
|
||||
const verb = resolveTurnStatusVerb(t, chatState, statusVerb)
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex w-fit items-center gap-[9px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-4 py-2 text-[13.5px] text-[var(--color-text-secondary)]">
|
||||
<div
|
||||
data-testid="turn-status-indicator"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="mb-2 flex w-fit items-center gap-[9px] rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-4 py-2 text-[13.5px] text-[var(--color-text-secondary)]"
|
||||
>
|
||||
<span className="animate-pulse-dot text-[var(--color-brand)]" aria-hidden="true">✦</span>
|
||||
<span className="font-medium text-[var(--color-text-primary)]">{verb}...</span>
|
||||
{elapsedSeconds > 0 && (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { BookMarked, ChevronDown, ChevronRight, CircleCheck, Settings } from 'lucide-react'
|
||||
import { ToolCallBlock } from './ToolCallBlock'
|
||||
import { ActivityGroup } from './ActivityGroup'
|
||||
import { ThinkingBlock } from './ThinkingBlock'
|
||||
import {
|
||||
activityStepToolCalls,
|
||||
toActivitySteps,
|
||||
@@ -163,7 +164,10 @@ function ToolCallGroupContent({
|
||||
}: ContentProps) {
|
||||
const toolCalls = activityStepToolCalls(steps)
|
||||
const hasImageGeneration = toolCalls.some((toolCall) => isImageGenerationToolName(toolCall.toolName))
|
||||
if (hasImageGeneration && !toolCalls.every((toolCall) => isImageGenerationToolName(toolCall.toolName))) {
|
||||
const hasNonImageSteps = steps.some(
|
||||
(step) => step.kind === 'thinking' || !isImageGenerationToolName(step.toolCall.toolName),
|
||||
)
|
||||
if (hasImageGeneration && hasNonImageSteps) {
|
||||
const segments: Array<
|
||||
| { kind: 'images'; toolCalls: ToolCall[] }
|
||||
| { kind: 'regular'; steps: ActivityStep[] }
|
||||
@@ -218,6 +222,20 @@ function ToolCallGroupContent({
|
||||
)
|
||||
}
|
||||
|
||||
if (toolCalls.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{steps.map((step) => step.kind === 'thinking' ? (
|
||||
<ThinkingBlock
|
||||
key={step.message.id}
|
||||
content={step.message.content}
|
||||
isActive={step.message.id === activeThinkingId}
|
||||
/>
|
||||
) : null)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const allAgents = toolCalls.length > 0 && toolCalls.every((toolCall) => toolCall.toolName === 'Agent')
|
||||
|
||||
if (allAgents) {
|
||||
|
||||
@@ -138,6 +138,51 @@ describe('chat blocks', () => {
|
||||
expect(screen.queryByRole('button', { name: /ToolSearch \(1\), ImageEdit \(1\)/ })).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps post-image thinking visible and live after the image result arrives', () => {
|
||||
const imageCall: Extract<UIMessage, { type: 'tool_use' }> = {
|
||||
id: 'image-use',
|
||||
type: 'tool_use',
|
||||
toolName: 'ImageGen',
|
||||
toolUseId: 'image-1',
|
||||
input: { prompt: 'A quiet mountain study' },
|
||||
timestamp: 1,
|
||||
}
|
||||
const thinking: Extract<UIMessage, { type: 'thinking' }> = {
|
||||
id: 'thinking-after-image',
|
||||
type: 'thinking',
|
||||
content: 'Check whether the generated image matches the requested composition.',
|
||||
timestamp: 3,
|
||||
}
|
||||
|
||||
render(
|
||||
<ToolCallGroup
|
||||
toolCalls={[imageCall]}
|
||||
steps={[
|
||||
{ kind: 'tool', toolCall: imageCall },
|
||||
{ kind: 'thinking', message: thinking },
|
||||
]}
|
||||
resultMap={new Map([[
|
||||
'image-1',
|
||||
{
|
||||
id: 'image-result',
|
||||
type: 'tool_result' as const,
|
||||
toolUseId: 'image-1',
|
||||
content: JSON.stringify({ type: 'image_generation_result', images: [] }),
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
]])}
|
||||
childToolCallsByParent={new Map()}
|
||||
agentTaskNotifications={{}}
|
||||
activeThinkingId="thinking-after-image"
|
||||
/>,
|
||||
)
|
||||
|
||||
const thinkingRow = screen.getByRole('button', { name: /Thinking/ })
|
||||
expect(thinkingRow).toBeTruthy()
|
||||
expect(thinkingRow.querySelector('.thinking-dots')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('replaces every image placeholder with the saved tool result', () => {
|
||||
const content = JSON.stringify({
|
||||
type: 'image_generation_result',
|
||||
|
||||
@@ -213,6 +213,49 @@ describe('ActiveSession task polling', () => {
|
||||
expect(screen.getByTestId('chat-input')).toHaveAttribute('data-variant', 'default')
|
||||
})
|
||||
|
||||
it('replaces the empty hero with the transcript as soon as first-turn preparation begins', () => {
|
||||
const sessionId = 'preparing-first-turn-session'
|
||||
useSessionStore.setState({
|
||||
sessions: [{
|
||||
id: sessionId,
|
||||
title: 'New Session',
|
||||
createdAt: '2026-08-07T00:00:00.000Z',
|
||||
modifiedAt: '2026-08-07T00:00:00.000Z',
|
||||
messageCount: 0,
|
||||
projectPath: '/workspace/project',
|
||||
workDir: '/workspace/project',
|
||||
workDirExists: true,
|
||||
}],
|
||||
activeSessionId: sessionId,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId, title: 'New Session', type: 'session', status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[sessionId]: {
|
||||
...useChatStore.getState().getSession(sessionId),
|
||||
connectionState: 'connected',
|
||||
historyStatus: 'ready',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
render(<ActiveSession />)
|
||||
expect(screen.getByTestId('empty-session-hero')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
useChatStore.getState().setPreparingTurn(sessionId, true)
|
||||
})
|
||||
|
||||
expect(screen.queryByTestId('empty-session-hero')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('message-list')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('chat-input')).toHaveAttribute('data-variant', 'default')
|
||||
})
|
||||
|
||||
it('shows the session token badge when usage is cache-only', () => {
|
||||
const sessionId = 'cache-only-token-session'
|
||||
|
||||
|
||||
@@ -387,6 +387,7 @@ export function ActiveSession() {
|
||||
const t = useTranslation()
|
||||
const messages = sessionState?.messages ?? EMPTY_MESSAGES
|
||||
const streamingText = sessionState?.streamingText ?? ''
|
||||
const isPreparingTurn = Boolean(sessionState?.isPreparingTurn)
|
||||
const backgroundTasks = useMemo(
|
||||
() => Object.values(sessionState?.backgroundAgentTasks ?? {}),
|
||||
[sessionState?.backgroundAgentTasks],
|
||||
@@ -397,7 +398,11 @@ export function ActiveSession() {
|
||||
)
|
||||
const agentTaskNotifications = sessionState?.agentTaskNotifications ?? EMPTY_AGENT_TASK_NOTIFICATIONS
|
||||
const activeGoal = sessionState?.activeGoal ?? null
|
||||
const isEmpty = messages.length === 0 && !streamingText && (session?.messageCount ?? 0) === 0
|
||||
const isEmpty =
|
||||
messages.length === 0 &&
|
||||
!streamingText &&
|
||||
!isPreparingTurn &&
|
||||
(session?.messageCount ?? 0) === 0
|
||||
const compactEmptyHero = isEmpty && showTerminalPanel
|
||||
const isHistoryLoading =
|
||||
!isMemberSession &&
|
||||
@@ -414,7 +419,7 @@ export function ActiveSession() {
|
||||
const visibleMessageCount = messages.length > 0 ? messages.length : session?.messageCount ?? 0
|
||||
const headerTitle = session?.title || t('session.untitled')
|
||||
|
||||
const isActive = chatState !== 'idle' || hasRunningBackgroundTasks
|
||||
const isActive = isPreparingTurn || chatState !== 'idle' || hasRunningBackgroundTasks
|
||||
const totalTokens = getTokenUsageTotal(tokenUsage)
|
||||
const cachedTokens = (tokenUsage.cache_read_tokens ?? 0) +
|
||||
(tokenUsage.cache_creation_tokens ?? 0)
|
||||
|
||||
@@ -106,6 +106,12 @@ type PendingComputerUsePermissions = Record<string, PendingComputerUsePermission
|
||||
export type PerSessionState = {
|
||||
messages: UIMessage[]
|
||||
chatState: ChatState
|
||||
/**
|
||||
* The first prompt is waiting for an empty placeholder session to be
|
||||
* replaced with its selected branch/worktree session. This is UI-only turn
|
||||
* state: no request has been sent to the old session.
|
||||
*/
|
||||
isPreparingTurn?: boolean
|
||||
connectionState: ConnectionState
|
||||
/** True after the server's authoritative reconnect snapshot has arrived. */
|
||||
connectionSnapshotReady?: boolean
|
||||
@@ -173,6 +179,7 @@ export type PerSessionState = {
|
||||
const DEFAULT_SESSION_STATE: PerSessionState = {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
isPreparingTurn: false,
|
||||
connectionState: 'disconnected',
|
||||
connectionSnapshotReady: false,
|
||||
historyStatus: 'idle',
|
||||
@@ -349,6 +356,7 @@ type ChatStore = {
|
||||
clearComposerDraft: (sessionId: string) => void
|
||||
setRepositoryLaunchDraft: (sessionId: string, draft: RepositoryLaunchDraftState) => void
|
||||
clearRepositoryLaunchDraft: (sessionId: string) => void
|
||||
setPreparingTurn: (sessionId: string, preparing: boolean) => void
|
||||
queueUserMessage: (
|
||||
sessionId: string,
|
||||
message: Omit<QueuedUserMessage, 'id' | 'createdAt'>,
|
||||
@@ -1559,6 +1567,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
...session,
|
||||
messages: newMessages,
|
||||
chatState: 'thinking',
|
||||
isPreparingTurn: false,
|
||||
historyMutationEpoch: (session.historyMutationEpoch ?? 0) + 1,
|
||||
elapsedSeconds: 0,
|
||||
suppressNextTaskNotificationResponse: false,
|
||||
@@ -2085,6 +2094,14 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
}))
|
||||
},
|
||||
|
||||
setPreparingTurn: (sessionId, preparing) => {
|
||||
set((state) => ({
|
||||
sessions: updateSessionIn(state.sessions, sessionId, () => ({
|
||||
isPreparingTurn: preparing,
|
||||
})),
|
||||
}))
|
||||
},
|
||||
|
||||
queueUserMessage: (sessionId, message) => {
|
||||
const id = `queued-user-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
set((state) => {
|
||||
|
||||
Reference in New Issue
Block a user