Skip to content
Merged
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
66 changes: 66 additions & 0 deletions src/core/parser-harnesses.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* 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 { describe, it, expect } from 'vitest';
import { hasExternalHarnessSources } from './parser-harnesses';

function setEnv(key: 'HOME' | 'USERPROFILE', value: string | undefined): void {
if (value === undefined) delete process.env[key]; else process.env[key] = value;
}

/** Run `body` with HOME/USERPROFILE pointed at a fresh temp dir, restoring the
* previous values (and removing the temp dir) afterwards. Self-contained so it
* leaks no env state across tests. */
function withHome(setup: (home: string) => void, body: () => void): void {
const prevHome = process.env.HOME;
const prevUserProfile = process.env.USERPROFILE;
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-home-'));
process.env.HOME = home;
process.env.USERPROFILE = home;
try {
setup(home);
body();
} finally {
setEnv('HOME', prevHome);
setEnv('USERPROFILE', prevUserProfile);
fs.rmSync(home, { recursive: true, force: true });
}
}

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);
});
});

it('returns false when no home directory is set (avoids relative-path probing)', () => {
const prevHome = process.env.HOME;
const prevUserProfile = process.env.USERPROFILE;
setEnv('HOME', undefined);
setEnv('USERPROFILE', undefined);
try {
expect(hasExternalHarnessSources()).toBe(false);
} finally {
setEnv('HOME', prevHome);
setEnv('USERPROFILE', prevUserProfile);
}
});
});
13 changes: 13 additions & 0 deletions src/core/parser-harnesses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ 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 {
// Without a home directory the find* helpers would join against an empty
// string and probe relative paths (e.g. `.claude/projects`) under the current
// working directory, which could report false positives. Bail out instead.
if (!process.env.HOME && !process.env.USERPROFILE) return false;
return findClaudeDirs().length > 0 || findCodexDirs().length > 0 || findOpenCodeDirs().length > 0;
}
Comment thread
bryantharpe marked this conversation as resolved.

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 (CLI and Xcode), Claude Code, Codex, and OpenCode sessions.'); } catch { /* disposed */ }
}
return;
}
Expand Down
Loading