Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/core/parser-harnesses.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
9 changes: 9 additions & 0 deletions src/core/parser-harnesses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ export interface ExternalHarnessProgressHandlers {
yieldToLoop?: () => Promise<void>;
}

/** 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) {
Expand Down
12 changes: 9 additions & 3 deletions src/webview/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down