) : fileTree.length === 0 ? (
@@ -504,10 +521,10 @@ function FileRow({
type="button"
onClick={onSelect}
style={{ paddingLeft: `${4 + Math.max(depth - 1, 0) * 16}px` }}
- className={`mb-0.5 flex min-h-8 w-full items-center gap-1.5 rounded-sm py-1 pr-2 text-left transition-colors focus:outline-none focus-visible:shadow-[var(--shadow-focus-ring)] ${
+ className={`mb-1 flex min-h-8 w-full items-center gap-1.5 rounded-md border py-1 pr-2 text-left transition-colors focus:outline-none focus-visible:shadow-[var(--shadow-focus-ring)] ${
active
- ? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
- : 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
+ ? 'border-[var(--color-memory-border)] bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
+ : 'border-transparent text-[var(--color-text-secondary)] hover:border-[var(--color-border)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]'
}`}
>
@@ -553,7 +570,7 @@ function MemoryTreeRow({
onClick={() => onToggleFolder(node.path)}
aria-expanded={!isCollapsed}
aria-label={t('settings.memory.toggleFolder', { name: node.name })}
- className="mb-0.5 flex min-h-8 w-full items-center gap-1.5 rounded-sm py-1 pr-2 text-left text-sm text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus:outline-none focus-visible:shadow-[var(--shadow-focus-ring)]"
+ className="mb-1 flex min-h-8 w-full items-center gap-1.5 rounded-md border border-transparent py-1 pr-2 text-left text-sm text-[var(--color-text-secondary)] transition-colors hover:border-[var(--color-border)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus:outline-none focus-visible:shadow-[var(--shadow-focus-ring)]"
style={{ paddingLeft: `${4 + Math.max(depth - 1, 0) * 16}px` }}
>
{isCollapsed ?
+
{node.children.map((child) => (
normalizeFsPath(file.path) === normalized)?.path ?? null
+}
+
type MemoryTreeNode =
| {
kind: 'folder'
diff --git a/src/server/__tests__/conversation-service.test.ts b/src/server/__tests__/conversation-service.test.ts
index 265c125f..264ac80d 100644
--- a/src/server/__tests__/conversation-service.test.ts
+++ b/src/server/__tests__/conversation-service.test.ts
@@ -81,9 +81,26 @@ describe('ConversationService', () => {
expect(env.ANTHROPIC_BASE_URL).toBe('https://example.invalid/anthropic')
expect(env.ANTHROPIC_MODEL).toBe('test-model')
expect(env.CLAUDE_CODE_DIAGNOSTICS_FILE).toBe(path.join(tmpDir, 'cc-haha', 'diagnostics', 'cli-diagnostics.jsonl'))
+ expect(env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE).toBe(
+ `${path.join(tmpDir, 'projects', 'D--workspace-code-myself-code-cc-haha', 'memory')}${path.sep}`,
+ )
await expect(fs.stat(path.dirname(env.CLAUDE_CODE_DIAGNOSTICS_FILE))).resolves.toBeTruthy()
})
+ test('buildChildEnv pins desktop memory to the current sanitized project directory', async () => {
+ const service = new ConversationService() as any
+ const workDir = path.join(tmpDir, 'workspace', 'myself_code', 'claude-code-haha')
+ await fs.mkdir(workDir, { recursive: true })
+
+ const env = (await service.buildChildEnv(workDir)) as Record
+
+ expect(env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE).toBe(
+ `${path.join(tmpDir, 'projects', sanitizeMemoryPath(workDir), 'memory')}${path.sep}`,
+ )
+ expect(env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE).toContain('myself-code')
+ expect(env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE).not.toContain('myself_code')
+ })
+
test('strips inherited provider env when desktop provider config exists', async () => {
const ccHahaDir = path.join(tmpDir, 'cc-haha')
await fs.mkdir(ccHahaDir, { recursive: true })
@@ -372,3 +389,7 @@ describe('ConversationService', () => {
expect(args).toContain('feature/rail')
})
})
+
+function sanitizeMemoryPath(value: string): string {
+ return value.replace(/[^a-zA-Z0-9]/g, '-')
+}
diff --git a/src/server/services/conversationService.ts b/src/server/services/conversationService.ts
index 70389b5e..51b1b846 100644
--- a/src/server/services/conversationService.ts
+++ b/src/server/services/conversationService.ts
@@ -22,11 +22,15 @@ import {
buildClaudeCliArgs,
resolveClaudeCliLauncher,
} from '../../utils/desktopBundledCli.js'
+import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
+import { findCanonicalGitRoot } from '../../utils/git.js'
+import { sanitizePath } from '../../utils/path.js'
const MAX_CAPTURED_PROCESS_LINES = 80
const MAX_CAPTURED_SDK_MESSAGES = 40
const MAX_CAPTURED_SDK_SUMMARY = 20
const CONTROL_READY_POLL_MS = 50
+const AUTO_MEMORY_DIRNAME = 'memory'
type AttachmentRef = {
type: 'file' | 'image'
@@ -900,6 +904,7 @@ export class ConversationService {
CLAUDE_CODE_ENABLE_TASKS: '1',
CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING: '1',
CLAUDE_CODE_DIAGNOSTICS_FILE: cliDiagnosticsPath,
+ CLAUDE_COWORK_MEMORY_PATH_OVERRIDE: this.resolveDesktopAutoMemoryPath(workDir),
CALLER_DIR: workDir,
PWD: workDir,
...(sdkUrl
@@ -933,6 +938,20 @@ export class ConversationService {
}
}
+ private resolveDesktopAutoMemoryPath(workDir: string): string {
+ const memoryProjectRoot = fs.existsSync(workDir)
+ ? findCanonicalGitRoot(workDir) ?? workDir
+ : workDir
+ return (
+ path.join(
+ getClaudeConfigHomeDir(),
+ 'projects',
+ sanitizePath(memoryProjectRoot),
+ AUTO_MEMORY_DIRNAME,
+ ) + path.sep
+ ).normalize('NFC')
+ }
+
/**
* 官方模式下构造 CLI 子进程的 auth env:
* - CLAUDE_CODE_ENTRYPOINT=claude-desktop 让 CLI 忽略外部残留 ANTHROPIC_* env