mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-14 11:04:44 +00:00
fix(claude): discover Claude Desktop/Cowork sessions in Windows MSIX installs
The desktop sessions resolver returned a single per-platform path, so Microsoft Store (MSIX) installs of Claude Desktop were invisible: their data lives under %LOCALAPPDATA%\Packages\<Claude package>\LocalCache\Roaming\Claude\local-agent-mode-sessions and a filesystem junction workaround breaks Cowork's own file access (reported and verified in #611). getDesktopSessionsDir() becomes getDesktopSessionsDirs(): an ordered, deduped candidate list (override, then classic APPDATA, then MSIX packages matching Claude_* or *.Claude_*, existence-checked, lexicographically sorted; .config on Linux). Results are memoized per env-input tuple so the parser's per-file classification never rescans Packages. All call sites scan every candidate; macOS, Linux and classic Windows behavior unchanged. Fixes #611
This commit is contained in:
parent
2f8e5bddcd
commit
d92b9fea43
6 changed files with 278 additions and 65 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
- Claude Desktop and Cowork sessions are discovered for Windows Microsoft Store (MSIX) installs. (#611)
|
||||
|
||||
## 0.9.19 - 2026-07-20
|
||||
|
||||
One version across every surface: CLI, macOS menubar, and the desktop app all ship as 0.9.19.
|
||||
|
|
|
|||
|
|
@ -12,11 +12,26 @@ Anthropic Claude Code CLI and Claude Desktop's local agent mode.
|
|||
|---|---|
|
||||
| Claude Code CLI | `$CLAUDE_CONFIG_DIR` if set, otherwise `~/.claude/projects/` |
|
||||
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/local-agent-mode-sessions/` |
|
||||
| Claude Desktop (Windows) | `%APPDATA%/Claude/local-agent-mode-sessions/` |
|
||||
| Claude Desktop (Windows, classic) | `%APPDATA%/Claude/local-agent-mode-sessions/` |
|
||||
| Claude Desktop (Windows, MSIX) | `%LOCALAPPDATA%/Packages/<Claude package>/LocalCache/Roaming/Claude/local-agent-mode-sessions/` |
|
||||
| Claude Desktop (Linux) | `~/.config/Claude/local-agent-mode-sessions/` |
|
||||
|
||||
For Desktop, `findDesktopProjectDirs` walks up to 8 levels deep looking for `projects/` subdirectories, skipping `node_modules` and `.git`.
|
||||
|
||||
Desktop session roots are resolved in this order:
|
||||
|
||||
1. A non-empty `CODEBURN_DESKTOP_SESSIONS_DIR` overrides discovery and is the
|
||||
only returned root.
|
||||
2. macOS uses the single path shown above.
|
||||
3. Windows always includes the classic path first. It then scans
|
||||
`%LOCALAPPDATA%/Packages` for package directories whose names start with
|
||||
`Claude_` or contain `.Claude_`, sorted by package name, and includes only
|
||||
packages whose full MSIX sessions path exists as a directory.
|
||||
4. Other platforms use the single Linux path shown above.
|
||||
|
||||
All returned roots are absolute, resolved, and deduplicated. Missing or
|
||||
unreadable Windows package directories are ignored.
|
||||
|
||||
## Storage format
|
||||
|
||||
JSONL, one event per line, per session file. Sessions live under `<project>/<sessionId>.jsonl`.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { normalizeContentBlocks } from './content-utils.js'
|
|||
import { discoverAllSessions, getProvider } from './providers/index.js'
|
||||
import { flushCodexCache } from './codex-cache.js'
|
||||
import { antigravityCascadeIdFromPath, flushAntigravityCache, shouldReparseAntigravitySource } from './providers/antigravity.js'
|
||||
import { getDesktopSessionsDir } from './providers/claude.js'
|
||||
import { getDesktopSessionsDirs } from './providers/claude.js'
|
||||
import { isSqliteBusyError } from './sqlite.js'
|
||||
import {
|
||||
type CachedCall,
|
||||
|
|
@ -81,9 +81,13 @@ function projectNameFromPath(projectPath: string, fallback: string): string {
|
|||
// In both cases the grouping key comes from the Cowork space name resolved in
|
||||
// claude.ts::discoverSessions().
|
||||
function isCoworkSession(cwd: string, filePath: string): boolean {
|
||||
const base = resolve(getDesktopSessionsDir())
|
||||
const inBase = (p: string) => p.startsWith(base + sep) || p.startsWith(base + '/')
|
||||
return inBase(resolve(cwd)) || inBase(resolve(filePath))
|
||||
const resolvedCwd = resolve(cwd)
|
||||
const resolvedFilePath = resolve(filePath)
|
||||
return getDesktopSessionsDirs().some(base => {
|
||||
const resolvedBase = resolve(base)
|
||||
const inBase = (p: string) => p.startsWith(resolvedBase + sep) || p.startsWith(resolvedBase + '/')
|
||||
return inBase(resolvedCwd) || inBase(resolvedFilePath)
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveCanonicalProjectPath(cwd: string): Promise<{ path: string; isWorktree: boolean }> {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { readdirSync, statSync } from 'fs'
|
||||
import { readFile, readdir, stat } from 'fs/promises'
|
||||
import { basename, delimiter as pathDelimiter, join, resolve } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
|
@ -101,15 +102,72 @@ export async function discoverClaudeConfigSources(): Promise<ClaudeConfigSource[
|
|||
})))
|
||||
}
|
||||
|
||||
export function getDesktopSessionsDir(): string {
|
||||
// Filesystem changes under an unchanged input key intentionally do not invalidate this cache.
|
||||
const desktopSessionsDirsCache = new Map<string, string[]>()
|
||||
|
||||
function cacheDesktopSessionsDirs(key: string, candidates: string[]): string[] {
|
||||
const dirs = dedupeResolved(candidates.map(candidate => resolve(candidate)))
|
||||
desktopSessionsDirsCache.set(key, dirs)
|
||||
return [...dirs]
|
||||
}
|
||||
|
||||
export function getDesktopSessionsDirs(): string[] {
|
||||
const override = process.env['CODEBURN_DESKTOP_SESSIONS_DIR']
|
||||
if (override) return override
|
||||
if (process.platform === 'darwin') return join(homedir(), 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions')
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env['APPDATA']?.trim()
|
||||
return join(appData || join(homedir(), 'AppData', 'Roaming'), 'Claude', 'local-agent-mode-sessions')
|
||||
const appDataInput = process.env['APPDATA']
|
||||
const localAppDataInput = process.env['LOCALAPPDATA']
|
||||
const platform = process.platform
|
||||
const cacheKey = JSON.stringify([platform, override ?? null, appDataInput ?? null, localAppDataInput ?? null])
|
||||
const cached = desktopSessionsDirsCache.get(cacheKey)
|
||||
if (cached) return [...cached]
|
||||
|
||||
if (override) return cacheDesktopSessionsDirs(cacheKey, [override])
|
||||
if (platform === 'darwin') {
|
||||
return cacheDesktopSessionsDirs(
|
||||
cacheKey,
|
||||
[join(homedir(), 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions')],
|
||||
)
|
||||
}
|
||||
return join(homedir(), '.config', 'Claude', 'local-agent-mode-sessions')
|
||||
if (platform === 'win32') {
|
||||
const appData = appDataInput?.trim()
|
||||
const candidates = [
|
||||
join(appData || join(homedir(), 'AppData', 'Roaming'), 'Claude', 'local-agent-mode-sessions'),
|
||||
]
|
||||
|
||||
const localAppData = localAppDataInput?.trim()
|
||||
const packagesDir = join(localAppData || join(homedir(), 'AppData', 'Local'), 'Packages')
|
||||
try {
|
||||
const entries = readdirSync(packagesDir, { withFileTypes: true })
|
||||
.filter(entry =>
|
||||
entry.isDirectory() &&
|
||||
(entry.name.startsWith('Claude_') || entry.name.includes('.Claude_')),
|
||||
)
|
||||
.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
||||
|
||||
for (const entry of entries) {
|
||||
const sessionsDir = join(
|
||||
packagesDir,
|
||||
entry.name,
|
||||
'LocalCache',
|
||||
'Roaming',
|
||||
'Claude',
|
||||
'local-agent-mode-sessions',
|
||||
)
|
||||
try {
|
||||
if (statSync(sessionsDir).isDirectory()) candidates.push(sessionsDir)
|
||||
} catch {
|
||||
// A package may disappear or be unreadable while Packages is scanned.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Missing or unreadable Packages is equivalent to no MSIX candidates.
|
||||
}
|
||||
|
||||
return cacheDesktopSessionsDirs(cacheKey, candidates)
|
||||
}
|
||||
return cacheDesktopSessionsDirs(
|
||||
cacheKey,
|
||||
[join(homedir(), '.config', 'Claude', 'local-agent-mode-sessions')],
|
||||
)
|
||||
}
|
||||
|
||||
async function findDesktopProjectDirs(base: string): Promise<string[]> {
|
||||
|
|
@ -221,7 +279,7 @@ export const claude: Provider = {
|
|||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
const dirs = await getClaudeConfigDirs()
|
||||
const roots: ProbeRoot[] = dirs.map(dir => ({ path: join(dir, 'projects'), label: 'projects' }))
|
||||
roots.push({ path: getDesktopSessionsDir(), label: 'desktop' })
|
||||
roots.push(...getDesktopSessionsDirs().map(path => ({ path, label: 'desktop' })))
|
||||
return roots
|
||||
},
|
||||
|
||||
|
|
@ -282,51 +340,52 @@ export const claude: Provider = {
|
|||
)
|
||||
}
|
||||
|
||||
const desktopBase = getDesktopSessionsDir()
|
||||
const desktopDirs = await findDesktopProjectDirs(desktopBase)
|
||||
const sep = desktopBase.includes('\\') ? '\\' : '/'
|
||||
// Desktop / Cowork sessions belong to no CLAUDE_CONFIG_DIR. Tag them with a
|
||||
// distinct source so a per-config view can account for them as their own
|
||||
// "Claude Desktop" bucket instead of silently dropping them (which made
|
||||
// sum-of-configs < All).
|
||||
const desktopSourceId = 'claude-desktop:' + createHash('sha256').update(resolve(desktopBase)).digest('hex').slice(0, 16)
|
||||
for (const dirPath of desktopDirs) {
|
||||
const resolved = resolve(dirPath)
|
||||
if (seenProjectDirs.has(resolved)) continue
|
||||
seenProjectDirs.add(resolved)
|
||||
for (const desktopBase of getDesktopSessionsDirs()) {
|
||||
const desktopDirs = await findDesktopProjectDirs(desktopBase)
|
||||
const sep = desktopBase.includes('\\') ? '\\' : '/'
|
||||
// Desktop / Cowork sessions belong to no CLAUDE_CONFIG_DIR. Tag them with a
|
||||
// distinct source so a per-config view can account for them as their own
|
||||
// "Claude Desktop" bucket instead of silently dropping them (which made
|
||||
// sum-of-configs < All).
|
||||
const desktopSourceId = 'claude-desktop:' + createHash('sha256').update(resolve(desktopBase)).digest('hex').slice(0, 16)
|
||||
for (const dirPath of desktopDirs) {
|
||||
const resolved = resolve(dirPath)
|
||||
if (seenProjectDirs.has(resolved)) continue
|
||||
seenProjectDirs.add(resolved)
|
||||
|
||||
// For Claude Desktop local-agent-mode (Cowork) sessions, the project dir
|
||||
// lives inside local_<sessionId>/.claude/projects/. We resolve the space
|
||||
// name from the sibling .json and spaces.json so it groups correctly.
|
||||
// Path structure: <desktopBase>/<appId>/<workspaceId>/local_<id>/.claude/projects/<slug>
|
||||
let projectName = basename(dirPath)
|
||||
const resolvedBase = resolve(desktopBase)
|
||||
if (resolved.startsWith(resolvedBase + sep) || resolved.startsWith(resolvedBase + '/')) {
|
||||
const rel = resolved.slice(resolvedBase.length + 1)
|
||||
const parts = rel.split(/[/\\]/)
|
||||
// parts = [appId, workspaceId, local_sessionId, .claude, projects, slug]
|
||||
if (
|
||||
parts.length >= 6 &&
|
||||
parts[2]?.startsWith('local_') &&
|
||||
parts[3] === '.claude' &&
|
||||
parts[4] === 'projects'
|
||||
) {
|
||||
const workspaceDir = join(resolvedBase, parts[0]!, parts[1]!)
|
||||
const sessionId = parts[2]!
|
||||
const spaceName = await resolveCoworkSpaceName(workspaceDir, sessionId)
|
||||
if (spaceName) projectName = spaceName
|
||||
// For Claude Desktop local-agent-mode (Cowork) sessions, the project dir
|
||||
// lives inside local_<sessionId>/.claude/projects/. We resolve the space
|
||||
// name from the sibling .json and spaces.json so it groups correctly.
|
||||
// Path structure: <desktopBase>/<appId>/<workspaceId>/local_<id>/.claude/projects/<slug>
|
||||
let projectName = basename(dirPath)
|
||||
const resolvedBase = resolve(desktopBase)
|
||||
if (resolved.startsWith(resolvedBase + sep) || resolved.startsWith(resolvedBase + '/')) {
|
||||
const rel = resolved.slice(resolvedBase.length + 1)
|
||||
const parts = rel.split(/[/\\]/)
|
||||
// parts = [appId, workspaceId, local_sessionId, .claude, projects, slug]
|
||||
if (
|
||||
parts.length >= 6 &&
|
||||
parts[2]?.startsWith('local_') &&
|
||||
parts[3] === '.claude' &&
|
||||
parts[4] === 'projects'
|
||||
) {
|
||||
const workspaceDir = join(resolvedBase, parts[0]!, parts[1]!)
|
||||
const sessionId = parts[2]!
|
||||
const spaceName = await resolveCoworkSpaceName(workspaceDir, sessionId)
|
||||
if (spaceName) projectName = spaceName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sources.push({
|
||||
path: dirPath,
|
||||
project: projectName,
|
||||
provider: 'claude',
|
||||
sourceId: desktopSourceId,
|
||||
sourceLabel: 'Claude Desktop',
|
||||
sourcePath: desktopBase,
|
||||
sourceKind: 'claude-desktop',
|
||||
})
|
||||
sources.push({
|
||||
path: dirPath,
|
||||
project: projectName,
|
||||
provider: 'claude',
|
||||
sourceId: desktopSourceId,
|
||||
sourceLabel: 'Claude Desktop',
|
||||
sourcePath: desktopBase,
|
||||
sourceKind: 'claude-desktop',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { type PeriodData, type ProviderCost, type BreakdownArrays, type MenubarP
|
|||
import { parseAllSessions, filterProjectsByName, filterProjectsByDays, filterProjectsByClaudeConfigSource, isSessionHydrationComplete } from './parser.js'
|
||||
import { findUnpricedModels, getLocalModelSavingsConfigHash, getPriceOverridesConfigHash, getShortModelName, isExpectedFreeModel } from './models.js'
|
||||
import { getAllProviders, safeDiscoverSessions } from './providers/index.js'
|
||||
import { claude, getClaudeConfigDirs, getDesktopSessionsDir } from './providers/claude.js'
|
||||
import { claude, getClaudeConfigDirs, getDesktopSessionsDirs } from './providers/claude.js'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { aggregateProjectsIntoDays, buildPeriodDataFromDays } from './day-aggregator.js'
|
||||
import { aggregateModelEfficiency } from './model-efficiency.js'
|
||||
|
|
@ -149,7 +149,11 @@ async function claudeConfigSelector(projects: ProjectSummary[], selectedId?: str
|
|||
// idle config or Claude Desktop is still selectable). Only worth it when a
|
||||
// second source is possible: more than one config dir, or a Claude Desktop
|
||||
// sessions dir exists. A plain single-config user skips it entirely.
|
||||
const desktopExists = await stat(getDesktopSessionsDir()).then(s => s.isDirectory()).catch(() => false)
|
||||
const desktopExists = (
|
||||
await Promise.all(
|
||||
getDesktopSessionsDirs().map(dir => stat(dir).then(s => s.isDirectory()).catch(() => false)),
|
||||
)
|
||||
).some(Boolean)
|
||||
if ((await getClaudeConfigDirs()).length > 1 || desktopExists) {
|
||||
for (const source of await claude.discoverSessions()) {
|
||||
if ((source.sourceKind !== 'claude-config' && source.sourceKind !== 'claude-desktop') || !source.sourceId || !source.sourceLabel || !source.sourcePath) continue
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { mkdtemp, mkdir, rm, writeFile } from 'fs/promises'
|
||||
import { delimiter as pathDelimiter, join } from 'path'
|
||||
import { delimiter as pathDelimiter, join, resolve, sep } from 'path'
|
||||
import { homedir, tmpdir } from 'os'
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
import { claude, getDesktopSessionsDir } from '../../src/providers/claude.js'
|
||||
import { claude, getDesktopSessionsDirs } from '../../src/providers/claude.js'
|
||||
import { clearSessionCache, filterProjectsByClaudeConfigSource, parseAllSessions } from '../../src/parser.js'
|
||||
|
||||
let tmpRoot: string
|
||||
|
|
@ -13,6 +13,7 @@ const savedEnv = {
|
|||
CLAUDE_CONFIG_DIRS: process.env['CLAUDE_CONFIG_DIRS'],
|
||||
CODEBURN_DESKTOP_SESSIONS_DIR: process.env['CODEBURN_DESKTOP_SESSIONS_DIR'],
|
||||
APPDATA: process.env['APPDATA'],
|
||||
LOCALAPPDATA: process.env['LOCALAPPDATA'],
|
||||
HOME: process.env['HOME'],
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ beforeEach(async () => {
|
|||
delete process.env['CLAUDE_CONFIG_DIRS']
|
||||
delete process.env['CODEBURN_DESKTOP_SESSIONS_DIR']
|
||||
delete process.env['APPDATA']
|
||||
delete process.env['LOCALAPPDATA']
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -218,30 +220,154 @@ describe('claude provider — CLAUDE_CONFIG_DIRS discovery', () => {
|
|||
})
|
||||
|
||||
describe('claude provider — Desktop sessions dir', () => {
|
||||
it('uses APPDATA as the Windows Claude Desktop sessions root', () => {
|
||||
async function makeMsixSessionsDir(localAppData: string, packageName: string): Promise<string> {
|
||||
const sessionsDir = join(
|
||||
localAppData,
|
||||
'Packages',
|
||||
packageName,
|
||||
'LocalCache',
|
||||
'Roaming',
|
||||
'Claude',
|
||||
'local-agent-mode-sessions',
|
||||
)
|
||||
await mkdir(sessionsDir, { recursive: true })
|
||||
return resolve(sessionsDir)
|
||||
}
|
||||
|
||||
it('returns the classic Windows root first and discovers an MSIX sessions root', async () => {
|
||||
const appData = join(tmpRoot, 'roaming-profile')
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
const classic = join(appData, 'Claude', 'local-agent-mode-sessions')
|
||||
const msix = await makeMsixSessionsDir(localAppData, 'Claude_fresh9k2m')
|
||||
await mkdir(classic, { recursive: true })
|
||||
process.env['APPDATA'] = appData
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([resolve(classic), msix])
|
||||
})
|
||||
})
|
||||
|
||||
it('filters MSIX package names and keeps both supported Claude name shapes', async () => {
|
||||
const appData = join(tmpRoot, 'roaming-profile')
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
const classic = resolve(join(appData, 'Claude', 'local-agent-mode-sessions'))
|
||||
const direct = await makeMsixSessionsDir(localAppData, 'Claude_new7v4p')
|
||||
const publisher = await makeMsixSessionsDir(localAppData, 'SomePublisher.Claude_q8w3e')
|
||||
await makeMsixSessionsDir(localAppData, 'ClaudeXtra_foo')
|
||||
await makeMsixSessionsDir(localAppData, 'NotClaude_x')
|
||||
process.env['APPDATA'] = appData
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([classic, direct, publisher])
|
||||
})
|
||||
})
|
||||
|
||||
it('returns only the classic root when LOCALAPPDATA is unset and Packages is missing', () => {
|
||||
const appData = join(tmpRoot, 'roaming-profile')
|
||||
process.env['APPDATA'] = appData
|
||||
delete process.env['LOCALAPPDATA']
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDir()).toBe(join(appData, 'Claude', 'local-agent-mode-sessions'))
|
||||
expect(() => getDesktopSessionsDirs()).not.toThrow()
|
||||
expect(getDesktopSessionsDirs()).toEqual([
|
||||
resolve(join(appData, 'Claude', 'local-agent-mode-sessions')),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the legacy Windows roaming profile path when APPDATA is unset', () => {
|
||||
it('falls back to the legacy Windows roaming path when APPDATA is unset or empty', () => {
|
||||
const expected = resolve(join(homedir(), 'AppData', 'Roaming', 'Claude', 'local-agent-mode-sessions'))
|
||||
delete process.env['APPDATA']
|
||||
delete process.env['LOCALAPPDATA']
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDir()).toBe(join(homedir(), 'AppData', 'Roaming', 'Claude', 'local-agent-mode-sessions'))
|
||||
expect(getDesktopSessionsDirs()).toEqual([expected])
|
||||
process.env['APPDATA'] = ''
|
||||
expect(getDesktopSessionsDirs()).toEqual([expected])
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps CODEBURN_DESKTOP_SESSIONS_DIR ahead of Windows APPDATA discovery', () => {
|
||||
it('excludes an MSIX package whose sessions subpath is missing', async () => {
|
||||
const appData = join(tmpRoot, 'roaming-profile')
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
await mkdir(join(localAppData, 'Packages', 'Claude_empty6r1t', 'LocalCache'), { recursive: true })
|
||||
process.env['APPDATA'] = appData
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([
|
||||
resolve(join(appData, 'Claude', 'local-agent-mode-sessions')),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('returns exactly the resolved override without platform discovery', async () => {
|
||||
const override = join(tmpRoot, 'desktop-override')
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
await makeMsixSessionsDir(localAppData, 'Claude_shouldnotappear5n8d')
|
||||
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = override
|
||||
process.env['APPDATA'] = join(tmpRoot, 'roaming-profile')
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDir()).toBe(override)
|
||||
expect(getDesktopSessionsDirs()).toEqual([resolve(override)])
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves an override containing a parent segment and trailing separator', () => {
|
||||
const override = join(tmpRoot, 'desktop-parent', 'nested') + `${sep}..${sep}sessions${sep}`
|
||||
process.env['CODEBURN_DESKTOP_SESSIONS_DIR'] = override
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([resolve(override)])
|
||||
})
|
||||
})
|
||||
|
||||
it('returns one platform root on darwin and linux without including MSIX packages', async () => {
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
await makeMsixSessionsDir(localAppData, 'Claude_crossplatform3b7j')
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('darwin', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([
|
||||
resolve(join(homedir(), 'Library', 'Application Support', 'Claude', 'local-agent-mode-sessions')),
|
||||
])
|
||||
})
|
||||
withPlatform('linux', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([
|
||||
resolve(join(homedir(), '.config', 'Claude', 'local-agent-mode-sessions')),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('sorts two valid MSIX roots by package entry name', async () => {
|
||||
const appData = join(tmpRoot, 'roaming-profile')
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
const later = await makeMsixSessionsDir(localAppData, 'Claude_zulu4c9x')
|
||||
const earlier = await makeMsixSessionsDir(localAppData, 'Alpha.Claude_alpha2m6v')
|
||||
process.env['APPDATA'] = appData
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([
|
||||
resolve(join(appData, 'Claude', 'local-agent-mode-sessions')),
|
||||
earlier,
|
||||
later,
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('dedupes classic and MSIX candidates that resolve to the same path', async () => {
|
||||
const localAppData = join(tmpRoot, 'local-profile')
|
||||
const packageName = 'Claude_duplicate8h3s'
|
||||
const sessionsDir = await makeMsixSessionsDir(localAppData, packageName)
|
||||
process.env['APPDATA'] = join(localAppData, 'Packages', packageName, 'LocalCache', 'Roaming')
|
||||
process.env['LOCALAPPDATA'] = localAppData
|
||||
|
||||
withPlatform('win32', () => {
|
||||
expect(getDesktopSessionsDirs()).toEqual([sessionsDir])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue