diff --git a/desktop/src/__tests__/memorySettings.test.tsx b/desktop/src/__tests__/memorySettings.test.tsx
index 131a4bec..efbeaa3f 100644
--- a/desktop/src/__tests__/memorySettings.test.tsx
+++ b/desktop/src/__tests__/memorySettings.test.tsx
@@ -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()
+
+ 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()
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx
index a9e37cb7..a3b47e76 100644
--- a/desktop/src/components/chat/MessageList.test.tsx
+++ b/desktop/src/components/chat/MessageList.test.tsx
@@ -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":"',
+ 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":"',
+ isPending: true,
+ timestamp: 1,
+ },
+ ],
+ }),
+ },
+ })
+
+ const { container } = render()
+ 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":"\\n',
+ streamingResponseChars: 32,
+ messages: [
+ {
+ ...state.sessions[ACTIVE_TAB]!.messages[0] as Extract,
+ input: { file_path: '/tmp/app.vue', content: '\n' },
+ partialInput: '{"file_path":"/tmp/app.vue","content":"\\n',
+ },
+ ],
+ },
+ },
+ }))
+ })
+
+ 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: {
diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx
index 99b578ad..b0bbbea8 100644
--- a/desktop/src/components/chat/MessageList.tsx
+++ b/desktop/src/components/chat/MessageList.tsx
@@ -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')
diff --git a/desktop/src/stores/memoryStore.ts b/desktop/src/stores/memoryStore.ts
index 1052e2b6..021dd1c2 100644
--- a/desktop/src/stores/memoryStore.ts
+++ b/desktop/src/stores/memoryStore.ts
@@ -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((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 })
}
diff --git a/src/server/__tests__/memory.test.ts b/src/server/__tests__/memory.test.ts
index f8d4c2b1..61faee74 100644
--- a/src/server/__tests__/memory.test.ts
+++ b/src/server/__tests__/memory.test.ts
@@ -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)
diff --git a/src/server/__tests__/scheduled-tasks.test.ts b/src/server/__tests__/scheduled-tasks.test.ts
index 382cb7ff..c49ac3e0 100644
--- a/src/server/__tests__/scheduled-tasks.test.ts
+++ b/src/server/__tests__/scheduled-tasks.test.ts
@@ -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([])
})
})
diff --git a/src/server/api/memory.ts b/src/server/api/memory.ts
index 2996cbc1..cb0b3684 100644
--- a/src/server/api/memory.ts
+++ b/src/server/api/memory.ts
@@ -114,7 +114,7 @@ async function listMemoryProjects(cwd?: string): Promise {
)
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