mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Tested: - bun test src/server/__tests__/memory.test.ts - cd desktop && bun run test -- src/__tests__/memorySettings.test.tsx - cd desktop && bun run test -- src/components/chat/MessageList.test.tsx -t "active tool input" - cd desktop && bun run check:desktop - bun run check:server
This commit is contained in:
parent
f5a97ac21c
commit
4b6c855eda
@ -163,6 +163,33 @@ describe('MemorySettings', () => {
|
||||
expect(await screen.findByTestId('markdown-preview')).toHaveTextContent('Prefer small diffs')
|
||||
})
|
||||
|
||||
it('does not select a missing current project with no memory files', async () => {
|
||||
memoryApiMock.listProjects.mockResolvedValue({
|
||||
projects: [
|
||||
{
|
||||
id: '-programs-claude-code',
|
||||
label: 'C:\\Programs\\claude-code',
|
||||
memoryDir: 'C:\\Users\\HUAWEI\\.claude\\projects\\-programs-claude-code\\memory',
|
||||
exists: false,
|
||||
fileCount: 0,
|
||||
isCurrent: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
render(<MemorySettings />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Programs/claude-code')).toHaveLength(1)
|
||||
})
|
||||
expect(screen.getByText('Missing')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('No file selected').length).toBeGreaterThan(0)
|
||||
expect(screen.getByText('Select a project.')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/HUAWEI/)).not.toBeInTheDocument()
|
||||
expect(memoryApiMock.listFiles).not.toHaveBeenCalled()
|
||||
expect(useMemoryStore.getState().selectedProjectId).toBeNull()
|
||||
})
|
||||
|
||||
it('lets the markdown editor fill the remaining detail pane height', async () => {
|
||||
render(<MemorySettings />)
|
||||
|
||||
|
||||
@ -2269,6 +2269,80 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(scrollTop).toBe(600)
|
||||
})
|
||||
|
||||
it('keeps auto-scrolling when active tool input updates in place', async () => {
|
||||
const scrollIntoView = vi.fn()
|
||||
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
|
||||
configurable: true,
|
||||
value: scrollIntoView,
|
||||
})
|
||||
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[ACTIVE_TAB]: makeSessionState({
|
||||
chatState: 'tool_executing',
|
||||
streamingToolInput: '{"file_path":"/tmp/app.vue","content":"<template>',
|
||||
activeToolUseId: 'write-1',
|
||||
activeToolName: 'Write',
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-write',
|
||||
type: 'tool_use',
|
||||
toolName: 'Write',
|
||||
toolUseId: 'write-1',
|
||||
input: {},
|
||||
partialInput: '{"file_path":"/tmp/app.vue","content":"<template>',
|
||||
isPending: true,
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const { container } = render(<MessageList />)
|
||||
const scroller = container.querySelector('.overflow-y-auto') as HTMLDivElement
|
||||
let scrollTop = 552
|
||||
Object.defineProperty(scroller, 'scrollHeight', { configurable: true, value: 1000 })
|
||||
Object.defineProperty(scroller, 'clientHeight', { configurable: true, value: 400 })
|
||||
Object.defineProperty(scroller, 'scrollTop', {
|
||||
configurable: true,
|
||||
get: () => scrollTop,
|
||||
set: (value) => {
|
||||
scrollTop = value
|
||||
},
|
||||
})
|
||||
|
||||
scrollIntoView.mockClear()
|
||||
await waitForProgrammaticScrollReset()
|
||||
fireEvent.scroll(scroller)
|
||||
|
||||
act(() => {
|
||||
useChatStore.setState((state) => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[ACTIVE_TAB]: {
|
||||
...state.sessions[ACTIVE_TAB]!,
|
||||
streamingToolInput: '{"file_path":"/tmp/app.vue","content":"<template>\\n<section>latest</section>',
|
||||
streamingResponseChars: 32,
|
||||
messages: [
|
||||
{
|
||||
...state.sessions[ACTIVE_TAB]!.messages[0] as Extract<UIMessage, { type: 'tool_use' }>,
|
||||
input: { file_path: '/tmp/app.vue', content: '<template>\n<section>latest</section>' },
|
||||
partialInput: '{"file_path":"/tmp/app.vue","content":"<template>\\n<section>latest</section>',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('2 lines · 36 chars')).toBeTruthy()
|
||||
})
|
||||
expect(scrollIntoView).not.toHaveBeenCalled()
|
||||
expect(scrollTop).toBe(600)
|
||||
})
|
||||
|
||||
it('keeps auto-scrolling without reading scroll geometry synchronously', async () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
|
||||
@ -1349,6 +1349,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
||||
const messages = sessionState?.messages ?? EMPTY_MESSAGES
|
||||
const chatState = sessionState?.chatState ?? 'idle'
|
||||
const streamingText = sessionState?.streamingText ?? ''
|
||||
const streamingToolInput = sessionState?.streamingToolInput ?? ''
|
||||
const activeThinkingId = sessionState?.activeThinkingId ?? null
|
||||
const agentTaskNotifications = sessionState?.agentTaskNotifications ?? EMPTY_AGENT_TASK_NOTIFICATIONS
|
||||
const activeAskUserQuestionToolUseId =
|
||||
@ -1603,7 +1604,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = {
|
||||
}
|
||||
|
||||
scrollToBottom('auto')
|
||||
}, [messages.length, resolvedSessionId, scrollToBottom, streamingText])
|
||||
}, [messages.length, resolvedSessionId, scrollToBottom, streamingText, streamingToolInput])
|
||||
|
||||
const handleJumpToLatest = useCallback(() => {
|
||||
scrollToBottom('auto')
|
||||
|
||||
@ -2,6 +2,10 @@ import { create } from 'zustand'
|
||||
import { memoryApi } from '../api/memory'
|
||||
import type { MemoryFile, MemoryFileDetail, MemoryProject } from '../types/memory'
|
||||
|
||||
function canSelectMemoryProject(project: MemoryProject): boolean {
|
||||
return project.exists || project.fileCount > 0
|
||||
}
|
||||
|
||||
type MemoryStore = {
|
||||
projects: MemoryProject[]
|
||||
files: MemoryFile[]
|
||||
@ -41,12 +45,26 @@ export const useMemoryStore = create<MemoryStore>((set, get) => ({
|
||||
set({ isLoadingProjects: true, error: null })
|
||||
try {
|
||||
const { projects } = await memoryApi.listProjects(cwd)
|
||||
const current = projects.find((project) => project.isCurrent)
|
||||
const selectableProjects = projects.filter(canSelectMemoryProject)
|
||||
const current = selectableProjects.find((project) => project.isCurrent)
|
||||
const previousSelectedProjectId = get().selectedProjectId
|
||||
const selectedProjectId =
|
||||
get().selectedProjectId && projects.some((project) => project.id === get().selectedProjectId)
|
||||
? get().selectedProjectId
|
||||
: current?.id ?? projects[0]?.id ?? null
|
||||
set({ projects, selectedProjectId, isLoadingProjects: false })
|
||||
previousSelectedProjectId && selectableProjects.some((project) => project.id === previousSelectedProjectId)
|
||||
? previousSelectedProjectId
|
||||
: current?.id ?? selectableProjects[0]?.id ?? null
|
||||
set({
|
||||
projects,
|
||||
selectedProjectId,
|
||||
isLoadingProjects: false,
|
||||
...(selectedProjectId === previousSelectedProjectId
|
||||
? {}
|
||||
: {
|
||||
files: [],
|
||||
selectedFile: null,
|
||||
draftContent: '',
|
||||
lastSavedAt: null,
|
||||
}),
|
||||
})
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message, isLoadingProjects: false })
|
||||
}
|
||||
|
||||
@ -40,6 +40,18 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('memory API', () => {
|
||||
it('does not fabricate a current memory project when no memory directory exists', async () => {
|
||||
const cwd = path.join(tmpDir, 'fresh-install', 'app')
|
||||
|
||||
const projectsRes = await request('GET', `/api/memory/projects?cwd=${encodeURIComponent(cwd)}`)
|
||||
expect(projectsRes.status).toBe(200)
|
||||
const projectsBody = await projectsRes.json() as {
|
||||
projects: Array<{ id: string; isCurrent: boolean; exists: boolean; fileCount: number }>
|
||||
}
|
||||
|
||||
expect(projectsBody.projects).toEqual([])
|
||||
})
|
||||
|
||||
it('lists current project memory files with frontmatter metadata', async () => {
|
||||
const cwd = path.join(tmpDir, 'workspace', 'app')
|
||||
const projectId = sanitizePath(cwd)
|
||||
|
||||
@ -241,7 +241,7 @@ describe('SearchService', () => {
|
||||
})
|
||||
|
||||
it('should return empty session results when no projects dir exists', async () => {
|
||||
const results = await service.searchSessions('test')
|
||||
const { results } = await service.searchSessions('test')
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@ -114,7 +114,7 @@ async function listMemoryProjects(cwd?: string): Promise<MemoryProject[]> {
|
||||
)
|
||||
|
||||
return resolved
|
||||
.filter((project) => project.isCurrent || project.exists || project.fileCount > 0)
|
||||
.filter((project) => project.exists)
|
||||
.sort((a, b) => {
|
||||
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1
|
||||
if (a.fileCount !== b.fileCount) return b.fileCount - a.fileCount
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user