mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: resolve TypeScript compilation errors in multi-tab refactor
Update all components, hooks, and tests to use the new per-session chatStore API where state is keyed by sessionId under `sessions` and all action methods require sessionId as the first parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e462314100
commit
9bfbadd170
1266
desktop/package-lock.json
generated
1266
desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,7 @@
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"jsdom": "^25.0.1",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.7",
|
||||
"vitest": "^3.0.4"
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ import { ToolInspection } from '../pages/ToolInspection'
|
||||
import { AppShell } from '../components/layout/AppShell'
|
||||
import { UserMessage } from '../components/chat/UserMessage'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
|
||||
/**
|
||||
* Core rendering tests: content-only pages must render without crashing
|
||||
@ -44,12 +45,32 @@ describe('Content-only pages render without errors', () => {
|
||||
})
|
||||
|
||||
it('ActiveSession shows a single primary action button while a turn is active', () => {
|
||||
useChatStore.setState({ chatState: 'thinking' })
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] })
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'active-tab': {
|
||||
messages: [],
|
||||
chatState: 'thinking',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
render(<ActiveSession />)
|
||||
|
||||
expect(screen.getByRole('button', { name: /stop/i })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /^run$/i })).not.toBeInTheDocument()
|
||||
useChatStore.setState({ chatState: 'idle' })
|
||||
useChatStore.setState({ sessions: {} })
|
||||
})
|
||||
|
||||
it('AgentTeams renders team strip and members', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { Button } from '../shared/Button'
|
||||
|
||||
@ -47,6 +48,7 @@ function parseInput(input: unknown): Question[] {
|
||||
|
||||
export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) {
|
||||
const { sendMessage } = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const t = useTranslation()
|
||||
const questions = parseInput(input)
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
@ -82,7 +84,8 @@ export function AskUserQuestion({ toolUseId: _toolUseId, input }: Props) {
|
||||
if (!response) return
|
||||
|
||||
setSubmitted(true)
|
||||
sendMessage(response)
|
||||
if (!activeTabId) return
|
||||
sendMessage(activeTabId, response)
|
||||
}
|
||||
|
||||
// All questions must be answered (via selection or free text) to enable submit
|
||||
|
||||
@ -2,53 +2,72 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { MessageList, buildRenderItems } from './MessageList'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import type { UIMessage } from '../../types/chat'
|
||||
import type { PerSessionState } from '../../stores/chatStore'
|
||||
|
||||
const ACTIVE_TAB = 'active-tab'
|
||||
|
||||
function makeSessionState(overrides: Partial<PerSessionState> = {}): PerSessionState {
|
||||
return {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
elapsedTimer: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('MessageList nested tool calls', () => {
|
||||
beforeEach(() => {
|
||||
useChatStore.setState({
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
elapsedSeconds: 0,
|
||||
})
|
||||
useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', status: 'idle' }] })
|
||||
useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState() } })
|
||||
})
|
||||
|
||||
it('renders sub-agent tool calls inline beneath the parent agent tool call', () => {
|
||||
useChatStore.setState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: 'Inspect src/components' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'tool-read',
|
||||
type: 'tool_use',
|
||||
toolName: 'Read',
|
||||
toolUseId: 'read-1',
|
||||
input: { file_path: '/tmp/example.ts' },
|
||||
timestamp: 2,
|
||||
parentToolUseId: 'agent-1',
|
||||
},
|
||||
{
|
||||
id: 'result-read',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'read-1',
|
||||
content: 'const answer = 42',
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
parentToolUseId: 'agent-1',
|
||||
},
|
||||
],
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: 'Inspect src/components' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'tool-read',
|
||||
type: 'tool_use',
|
||||
toolName: 'Read',
|
||||
toolUseId: 'read-1',
|
||||
input: { file_path: '/tmp/example.ts' },
|
||||
timestamp: 2,
|
||||
parentToolUseId: 'agent-1',
|
||||
},
|
||||
{
|
||||
id: 'result-read',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'read-1',
|
||||
content: 'const answer = 42',
|
||||
isError: false,
|
||||
timestamp: 3,
|
||||
parentToolUseId: 'agent-1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const { container } = render(<MessageList />)
|
||||
@ -106,24 +125,28 @@ describe('MessageList nested tool calls', () => {
|
||||
|
||||
it('shows failed agent status and compact unavailable summary for Explore launch errors', () => {
|
||||
useChatStore.setState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: '探索整体架构', subagent_type: 'Explore' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: `Agent type 'Explore' not found. Available agents: general-purpose`,
|
||||
isError: true,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: '探索整体架构', subagent_type: 'Explore' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: `Agent type 'Explore' not found. Available agents: general-purpose`,
|
||||
isError: true,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
@ -136,27 +159,31 @@ describe('MessageList nested tool calls', () => {
|
||||
const longResult = '探索完成。让我将结果整合写入计划文件。第二段补充内容用于验证 dialog 展示的是完整结果而不是截断摘要。'
|
||||
|
||||
useChatStore.setState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: '探索整体架构' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: {
|
||||
status: 'completed',
|
||||
content: [{ type: 'text', text: longResult }],
|
||||
},
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: '探索整体架构' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: {
|
||||
status: 'completed',
|
||||
content: [{ type: 'text', text: longResult }],
|
||||
},
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
@ -180,26 +207,30 @@ describe('MessageList nested tool calls', () => {
|
||||
})
|
||||
|
||||
useChatStore.setState({
|
||||
messages: [
|
||||
{
|
||||
id: 'user-1',
|
||||
type: 'user_text',
|
||||
content: '请帮我探索整体架构',
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'assistant-1',
|
||||
type: 'assistant_text',
|
||||
content: '先看 CLI 和服务端入口。',
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
id: 'assistant-2',
|
||||
type: 'assistant_text',
|
||||
content: '再看 desktop 前后端边界。',
|
||||
timestamp: 3,
|
||||
},
|
||||
],
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
messages: [
|
||||
{
|
||||
id: 'user-1',
|
||||
type: 'user_text',
|
||||
content: '请帮我探索整体架构',
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'assistant-1',
|
||||
type: 'assistant_text',
|
||||
content: '先看 CLI 和服务端入口。',
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
id: 'assistant-2',
|
||||
type: 'assistant_text',
|
||||
content: '再看 desktop 前后端边界。',
|
||||
timestamp: 3,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { Button } from '../shared/Button'
|
||||
import { DiffViewer } from './DiffViewer'
|
||||
@ -110,7 +111,9 @@ function renderPermissionPreview(toolName: string, input: unknown) {
|
||||
}
|
||||
|
||||
export function PermissionDialog({ requestId, toolName, input, description }: Props) {
|
||||
const { respondToPermission, pendingPermission } = useChatStore()
|
||||
const { respondToPermission } = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const pendingPermission = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.pendingPermission : undefined)
|
||||
const t = useTranslation()
|
||||
const isPending = pendingPermission?.requestId === requestId
|
||||
const [showRaw, setShowRaw] = useState(false)
|
||||
@ -223,7 +226,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => respondToPermission(requestId, true)}
|
||||
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, true)}
|
||||
icon={
|
||||
<span className="material-symbols-outlined text-[14px]">check</span>
|
||||
}
|
||||
@ -233,7 +236,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => respondToPermission(requestId, true, 'always')}
|
||||
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, true, 'always')}
|
||||
icon={
|
||||
<span className="material-symbols-outlined text-[14px]">verified</span>
|
||||
}
|
||||
@ -244,7 +247,7 @@ export function PermissionDialog({ requestId, toolName, input, description }: Pr
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => respondToPermission(requestId, false)}
|
||||
onClick={() => activeTabId && respondToPermission(activeTabId, requestId, false)}
|
||||
icon={
|
||||
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { TranslationKey } from '../../i18n/locales/en'
|
||||
|
||||
@ -10,7 +11,12 @@ function formatElapsed(seconds: number): string {
|
||||
}
|
||||
|
||||
export function StreamingIndicator() {
|
||||
const { chatState, statusVerb, elapsedSeconds, tokenUsage } = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const statusVerb = sessionState?.statusVerb ?? ''
|
||||
const elapsedSeconds = sessionState?.elapsedSeconds ?? 0
|
||||
const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 }
|
||||
const t = useTranslation()
|
||||
|
||||
// Translate known server-sent verbs (e.g. "Thinking", "Task started")
|
||||
|
||||
@ -4,20 +4,12 @@ import { ThinkingBlock } from './ThinkingBlock'
|
||||
import { ToolCallBlock } from './ToolCallBlock'
|
||||
import { PermissionDialog } from './PermissionDialog'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
|
||||
describe('chat blocks', () => {
|
||||
beforeEach(() => {
|
||||
useChatStore.setState({
|
||||
chatState: 'idle',
|
||||
messages: [],
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
elapsedSeconds: 0,
|
||||
})
|
||||
useTabStore.setState({ activeTabId: 'active-tab', tabs: [{ sessionId: 'active-tab', title: 'Test', status: 'idle' }] })
|
||||
useChatStore.setState({ sessions: {} })
|
||||
})
|
||||
|
||||
it('keeps thinking collapsed by default', () => {
|
||||
@ -72,13 +64,30 @@ describe('chat blocks', () => {
|
||||
|
||||
it('shows a diff preview for edit permission requests', () => {
|
||||
useChatStore.setState({
|
||||
pendingPermission: {
|
||||
requestId: 'perm-1',
|
||||
toolName: 'Edit',
|
||||
input: {
|
||||
file_path: '/tmp/example.ts',
|
||||
old_string: 'const count = 1',
|
||||
new_string: 'const count = 2',
|
||||
sessions: {
|
||||
'active-tab': {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: {
|
||||
requestId: 'perm-1',
|
||||
toolName: 'Edit',
|
||||
input: {
|
||||
file_path: '/tmp/example.ts',
|
||||
old_string: 'const count = 1',
|
||||
new_string: 'const count = 2',
|
||||
},
|
||||
},
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { PermissionMode } from '../../types/settings'
|
||||
|
||||
@ -26,6 +27,7 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange }
|
||||
const t = useTranslation()
|
||||
const { permissionMode: storeMode, setPermissionMode } = useSettingsStore()
|
||||
const setSessionPermissionMode = useChatStore((s) => s.setSessionPermissionMode)
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||
const [open, setOpen] = useState(false)
|
||||
@ -126,7 +128,7 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange }
|
||||
onChange?.(item.value)
|
||||
} else {
|
||||
void setPermissionMode(item.value)
|
||||
setSessionPermissionMode(item.value)
|
||||
if (activeTabId) setSessionPermissionMode(activeTabId, item.value)
|
||||
}
|
||||
setOpen(false)
|
||||
}}
|
||||
@ -208,7 +210,7 @@ export function PermissionModeSelector({ workDir: workDirProp, value, onChange }
|
||||
onChange?.('bypassPermissions')
|
||||
} else {
|
||||
void setPermissionMode('bypassPermissions')
|
||||
setSessionPermissionMode('bypassPermissions')
|
||||
if (activeTabId) setSessionPermissionMode(activeTabId, 'bypassPermissions')
|
||||
}
|
||||
setConfirmDialog(false)
|
||||
}}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { useTabStore } from '../../stores/tabStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
|
||||
export function StatusBar() {
|
||||
const { currentModel } = useSettingsStore()
|
||||
const { connectionState } = useChatStore()
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const connectionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.connectionState ?? 'disconnected' : 'disconnected')
|
||||
const { sessions, activeSessionId } = useSessionStore()
|
||||
const t = useTranslation()
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSessionStore } from '../stores/sessionStore'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import { useTabStore } from '../stores/tabStore'
|
||||
import { useUIStore } from '../stores/uiStore'
|
||||
|
||||
export function useKeyboardShortcuts() {
|
||||
@ -9,7 +10,8 @@ export function useKeyboardShortcuts() {
|
||||
const closeModal = useUIStore((s) => s.closeModal)
|
||||
const activeModal = useUIStore((s) => s.activeModal)
|
||||
const stopGeneration = useChatStore((s) => s.stopGeneration)
|
||||
const chatState = useChatStore((s) => s.chatState)
|
||||
const activeTabId = useTabStore((s) => s.activeTabId)
|
||||
const chatState = useChatStore((s) => activeTabId ? s.sessions[activeTabId]?.chatState ?? 'idle' : 'idle')
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
@ -38,14 +40,14 @@ export function useKeyboardShortcuts() {
|
||||
|
||||
// Cmd+. — Stop generation
|
||||
if (meta && e.key === '.') {
|
||||
if (chatState !== 'idle') {
|
||||
if (chatState !== 'idle' && activeTabId) {
|
||||
e.preventDefault()
|
||||
stopGeneration()
|
||||
stopGeneration(activeTabId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handler)
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [activeModal, chatState, closeModal, setActiveSession, setActiveView, stopGeneration])
|
||||
}, [activeModal, activeTabId, chatState, closeModal, setActiveSession, setActiveView, stopGeneration])
|
||||
}
|
||||
|
||||
@ -32,8 +32,39 @@ vi.mock('./teamStore', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./tabStore', () => ({
|
||||
useTabStore: {
|
||||
getState: () => ({
|
||||
updateTabStatus: vi.fn(),
|
||||
updateTabTitle: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./sessionStore', () => ({
|
||||
useSessionStore: {
|
||||
getState: () => ({
|
||||
updateSessionTitle: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./cliTaskStore', () => ({
|
||||
useCLITaskStore: {
|
||||
getState: () => ({
|
||||
fetchSessionTasks: vi.fn(),
|
||||
tasks: [],
|
||||
clearTasks: vi.fn(),
|
||||
setTasksFromTodos: vi.fn(),
|
||||
markCompletedAndDismissed: vi.fn(),
|
||||
refreshTasks: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
import { mapHistoryMessagesToUiMessages, useChatStore } from './chatStore'
|
||||
|
||||
const TEST_SESSION_ID = 'test-session-1'
|
||||
const initialState = useChatStore.getState()
|
||||
|
||||
describe('chatStore history mapping', () => {
|
||||
@ -41,19 +72,7 @@ describe('chatStore history mapping', () => {
|
||||
sendMock.mockReset()
|
||||
useChatStore.setState({
|
||||
...initialState,
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'disconnected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
connectedSessionId: null,
|
||||
slashCommands: [],
|
||||
sessions: {},
|
||||
})
|
||||
})
|
||||
|
||||
@ -95,7 +114,29 @@ describe('chatStore history mapping', () => {
|
||||
})
|
||||
|
||||
it('keeps parent tool linkage for live tool events', () => {
|
||||
useChatStore.getState().handleServerMessage({
|
||||
// Initialize the session first
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'tool_use_complete',
|
||||
toolName: 'Read',
|
||||
toolUseId: 'tool-1',
|
||||
@ -103,7 +144,7 @@ describe('chatStore history mapping', () => {
|
||||
parentToolUseId: 'agent-1',
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage({
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'tool_result',
|
||||
toolUseId: 'tool-1',
|
||||
content: 'ok',
|
||||
@ -111,7 +152,7 @@ describe('chatStore history mapping', () => {
|
||||
parentToolUseId: 'agent-1',
|
||||
})
|
||||
|
||||
expect(useChatStore.getState().messages).toMatchObject([
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||
{
|
||||
type: 'tool_use',
|
||||
toolUseId: 'tool-1',
|
||||
@ -126,13 +167,32 @@ describe('chatStore history mapping', () => {
|
||||
})
|
||||
|
||||
it('sends permission mode updates to the active session only', () => {
|
||||
useChatStore.getState().setSessionPermissionMode('acceptEdits')
|
||||
useChatStore.getState().setSessionPermissionMode('nonexistent-session', 'acceptEdits')
|
||||
expect(sendMock).not.toHaveBeenCalled()
|
||||
|
||||
useChatStore.setState({ connectedSessionId: 'session-1' })
|
||||
useChatStore.getState().setSessionPermissionMode('acceptEdits')
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
'session-1': {
|
||||
messages: [],
|
||||
chatState: 'idle',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
useChatStore.getState().setSessionPermissionMode('session-1', 'acceptEdits')
|
||||
|
||||
expect(sendMock).toHaveBeenCalledWith({
|
||||
expect(sendMock).toHaveBeenCalledWith('session-1', {
|
||||
type: 'set_permission_mode',
|
||||
mode: 'acceptEdits',
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user