diff --git a/src/core/parser-harnesses.test.ts b/src/core/parser-harnesses.test.ts new file mode 100644 index 0000000..c52b4a8 --- /dev/null +++ b/src/core/parser-harnesses.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Tests for external-harness source discovery used by the dashboard load gate, + * so a host with only non-VS Code logs (e.g. Claude Code on a headless + * Remote-SSH box, with no VS Code/Copilot directories) still loads. */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterEach, describe, it, expect } from 'vitest'; +import { hasExternalHarnessSources } from './parser-harnesses'; + +const ORIG_HOME = process.env.HOME; +const ORIG_USERPROFILE = process.env.USERPROFILE; + +function withHome(setup: (home: string) => void, run: () => void): void { + const home = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-home-')); + process.env.HOME = home; + process.env.USERPROFILE = home; + try { + setup(home); + run(); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +} + +afterEach(() => { + if (ORIG_HOME === undefined) delete process.env.HOME; else process.env.HOME = ORIG_HOME; + if (ORIG_USERPROFILE === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = ORIG_USERPROFILE; +}); + +describe('hasExternalHarnessSources', () => { + it('returns false when no external-harness directories exist', () => { + withHome(() => { /* empty home */ }, () => { + expect(hasExternalHarnessSources()).toBe(false); + }); + }); + + it('returns true when a Claude Code projects directory exists', () => { + withHome(home => { + fs.mkdirSync(path.join(home, '.claude', 'projects'), { recursive: true }); + }, () => { + expect(hasExternalHarnessSources()).toBe(true); + }); + }); +}); diff --git a/src/core/parser-harnesses.ts b/src/core/parser-harnesses.ts index a09adac..bf9861d 100644 --- a/src/core/parser-harnesses.ts +++ b/src/core/parser-harnesses.ts @@ -78,6 +78,15 @@ export interface ExternalHarnessProgressHandlers { yieldToLoop?: () => Promise; } +/** Returns true if any external-harness (Claude Code, Codex, OpenCode) session + * source exists on disk. The dashboard uses this so it does not abort when the + * only available logs come from a non-VS Code harness — e.g. a headless + * Remote-SSH host that has Claude Code sessions under `~/.claude/projects` but + * no VS Code workspace storage or Copilot directories. */ +export function hasExternalHarnessSources(): boolean { + return findClaudeDirs().length > 0 || findCodexDirs().length > 0 || findOpenCodeDirs().length > 0; +} + export function collectExternalHarnessesSync(workspaces: WorkspaceMap, sessions: Session[]): void { const ctx: HarnessCollectionContext = { workspaces, sessions }; for (const harness of EXTERNAL_HARNESSES) { diff --git a/src/webview/panel.ts b/src/webview/panel.ts index 4d73624..fbe7a53 100644 --- a/src/webview/panel.ts +++ b/src/webview/panel.ts @@ -9,6 +9,7 @@ import * as vscode from 'vscode'; import { Analyzer } from '../core/analyzer'; import { saveSidebarStats } from '../core/cache'; import { clearCache, findLogsDirs, parseAllLogsViaWorker, ParseResult } from '../core/parser'; +import { hasExternalHarnessSources } from '../core/parser-harnesses'; import { runtimeDebug } from '../core/runtime-debug'; import { WebviewMessage } from '../core/types'; import { panelCache } from './panel-cache'; @@ -203,11 +204,16 @@ export class DashboardPanel { if (this.disposed) return; const dirs = findLogsDirs(); - runtimeDebug('panel', 'logs-dirs-found', `count=${dirs.length}`); - if (dirs.length === 0) { + const hasExternal = hasExternalHarnessSources(); + runtimeDebug('panel', 'logs-dirs-found', `count=${dirs.length} external=${hasExternal}`); + // External harnesses (Claude Code, Codex, OpenCode) are collected by the + // parse worker independently of `dirs`, so only abort when no source of + // any kind is present. Otherwise a host with e.g. only Claude Code logs + // (and no VS Code/Copilot directories) would never load. + if (dirs.length === 0 && !hasExternal) { runtimeDebug('panel', 'loadData-no-dirs'); if (!this.disposed) { - try { this.panel.webview.html = getErrorHtml('No Copilot chat log directories found.'); } catch { /* disposed */ } + try { this.panel.webview.html = getErrorHtml('No AI coding session logs found. Looked for VS Code, GitHub Copilot, Claude Code, Codex, and OpenCode sessions.'); } catch { /* disposed */ } } return; }