From 58ebb44aeed6ac569a2ca8139064099ba9888a88 Mon Sep 17 00:00:00 2001 From: Bryan Tharpe Date: Mon, 25 May 2026 03:00:10 +0000 Subject: [PATCH] fix: load dashboard when only non-VS Code harness logs exist findLogsDirs() scans only VS Code workspace storage and Copilot directories, so the dashboard's load gate aborts with "No Copilot chat log directories found" whenever those are absent. But the Claude Code / Codex / OpenCode collectors run in the parse worker independently of that list, so a host with only ~/.claude/projects (e.g. a headless Remote-SSH machine with no VS Code workspace storage) never loads despite having valid sessions. Add hasExternalHarnessSources() and gate on it so the panel only aborts when no source of any kind is present. Update the Copilot-specific empty-state message to reflect all supported sources. --- src/core/parser-harnesses.test.ts | 50 +++++++++++++++++++++++++++++++ src/core/parser-harnesses.ts | 9 ++++++ src/webview/panel.ts | 12 ++++++-- 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/core/parser-harnesses.test.ts 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; }