diff --git a/Models.cs b/Models.cs
index 45fe13b..578431d 100644
--- a/Models.cs
+++ b/Models.cs
@@ -49,6 +49,28 @@ class EvalData
enum PagerAction { Quit, Browse, Resume }
+enum SessionDbType { CopilotCli, SkillValidator, Unknown }
+
+record BrowserSession(
+ string Id,
+ string Summary,
+ string Cwd,
+ DateTime UpdatedAt,
+ string EventsPath,
+ long FileSize,
+ string Branch,
+ string Repository,
+ SessionDbType DbType = SessionDbType.CopilotCli,
+ string? SkillName = null,
+ string? ScenarioName = null,
+ string? Role = null,
+ string? Model = null,
+ string? Status = null,
+ string? Prompt = null,
+ string? MetricsJson = null,
+ string? JudgeJson = null,
+ string? PairwiseJson = null);
+
// ========== JSON output records ==========
record TurnOutput(
int turn,
diff --git a/SessionBrowser.cs b/SessionBrowser.cs
index 071b497..a11b85d 100644
--- a/SessionBrowser.cs
+++ b/SessionBrowser.cs
@@ -8,22 +8,68 @@
class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessionStateDir)
{
- public List<(string id, string summary, string cwd, DateTime updatedAt, string eventsPath, long fileSize, string branch, string repository)>? LoadSessionsFromDb(string sessionStateDir, string? dbPathOverride = null)
- {
- var dbPath = dbPathOverride ?? Path.Combine(Path.GetDirectoryName(sessionStateDir)!, "session-store.db");
- if (!File.Exists(dbPath)) return null;
+ SessionDbType _currentDbType = SessionDbType.CopilotCli;
+ /// Detect DB type by inspecting schema tables.
+ public static SessionDbType DetectDbType(string dbPath)
+ {
+ if (!File.Exists(dbPath)) return SessionDbType.Unknown;
try
{
using var conn = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
conn.Open();
- // Validate schema version
+ // Check for skill-validator schema_info table
+ using (var cmd = conn.CreateCommand())
+ {
+ cmd.CommandText = "SELECT value FROM schema_info WHERE key='type' LIMIT 1";
+ try
+ {
+ var val = cmd.ExecuteScalar();
+ if (val is string s && s == "skill-validator")
+ return SessionDbType.SkillValidator;
+ }
+ catch { /* table doesn't exist */ }
+ }
+
+ // Check for Copilot CLI schema_version table
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'";
- if (cmd.ExecuteScalar() == null) return null;
+ if (cmd.ExecuteScalar() != null) return SessionDbType.CopilotCli;
}
+
+ return SessionDbType.Unknown;
+ }
+ catch { return SessionDbType.Unknown; }
+ }
+
+ public List? LoadSessionsFromDb(string sessionStateDir, string? dbPathOverride = null)
+ {
+ var dbPath = dbPathOverride ?? Path.Combine(Path.GetDirectoryName(sessionStateDir)!, "session-store.db");
+ var dbType = DetectDbType(dbPath);
+
+ if (dbType == SessionDbType.SkillValidator)
+ {
+ _currentDbType = SessionDbType.SkillValidator;
+ return LoadSkillValidatorSessions(dbPath);
+ }
+
+ if (dbType != SessionDbType.CopilotCli) return null;
+ _currentDbType = SessionDbType.CopilotCli;
+ return LoadCopilotCliSessions(sessionStateDir, dbPath);
+ }
+
+ List? LoadCopilotCliSessions(string sessionStateDir, string dbPath)
+ {
+ if (!File.Exists(dbPath)) return null;
+
+ try
+ {
+ using var conn = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
+ conn.Open();
+
+ // Validate schema version
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT version FROM schema_version LIMIT 1";
@@ -43,7 +89,7 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
}
// Load sessions
- var results = new List<(string id, string summary, string cwd, DateTime updatedAt, string eventsPath, long fileSize, string branch, string repository)>();
+ var results = new List();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT id, cwd, summary, updated_at, branch, repository FROM sessions ORDER BY updated_at DESC";
@@ -66,14 +112,117 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
else
continue; // skip DB entries without local transcript files
- results.Add((id, summary, cwd, updatedAt, eventsPath, fileSize, branch, repository));
+ results.Add(new BrowserSession(id, summary, cwd, updatedAt, eventsPath, fileSize, branch, repository));
}
}
return results;
}
catch
{
- return null; // any error โ fall back to file scan
+ return null;
+ }
+ }
+
+ List? LoadSkillValidatorSessions(string dbPath)
+ {
+ if (!File.Exists(dbPath)) return null;
+ var dbDir = Path.GetDirectoryName(Path.GetFullPath(dbPath))!;
+
+ try
+ {
+ using var conn = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
+ conn.Open();
+
+ var results = new List();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = """
+ SELECT s.id, s.skill_name, s.skill_path, s.scenario_name, s.run_index, s.role, s.model,
+ s.config_dir, s.work_dir, s.prompt, s.status, s.started_at, s.completed_at,
+ r.metrics_json, r.judge_json, r.pairwise_json
+ FROM sessions s
+ LEFT JOIN run_results r ON s.id = r.session_id
+ ORDER BY s.skill_name, s.scenario_name, s.run_index, s.role
+ """;
+ using var reader = cmd.ExecuteReader();
+ while (reader.Read())
+ {
+ var id = reader.GetString(0);
+ var skillName = reader.GetString(1);
+ var skillPath = reader.GetString(2);
+ var scenarioName = reader.GetString(3);
+ var runIndex = reader.GetInt32(4);
+ var role = reader.GetString(5);
+ var model = reader.GetString(6);
+ var configDir = reader.IsDBNull(7) ? null : reader.GetString(7);
+ var workDir = reader.IsDBNull(8) ? null : reader.GetString(8);
+ var prompt = reader.IsDBNull(9) ? null : reader.GetString(9);
+ var status = reader.GetString(10);
+ var startedAtStr = reader.GetString(11);
+ var completedAtStr = reader.IsDBNull(12) ? null : reader.GetString(12);
+ var metricsJson = reader.IsDBNull(13) ? null : reader.GetString(13);
+ var judgeJson = reader.IsDBNull(14) ? null : reader.GetString(14);
+ var pairwiseJson = reader.IsDBNull(15) ? null : reader.GetString(15);
+
+ var dateStr = completedAtStr ?? startedAtStr;
+ DateTime.TryParse(dateStr, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var updatedAt);
+
+ // Resolve events.jsonl: config_dir is relative to DB directory
+ // Normalize path separators for cross-platform (Windows backslashes โ forward slashes)
+ var eventsPath = "";
+ long fileSize = 0;
+ if (configDir is not null)
+ {
+ var normalizedConfigDir = configDir.Replace('\\', '/');
+ var configFullPath = Path.Combine(dbDir, normalizedConfigDir);
+
+ // Try direct: config_dir/events.jsonl
+ eventsPath = Path.Combine(configFullPath, "events.jsonl");
+ if (!File.Exists(eventsPath))
+ {
+ // Try nested: config_dir/session-state/*/events.jsonl
+ var sessionStateDir2 = Path.Combine(configFullPath, "session-state");
+ if (Directory.Exists(sessionStateDir2))
+ {
+ foreach (var subDir in Directory.GetDirectories(sessionStateDir2))
+ {
+ var candidate = Path.Combine(subDir, "events.jsonl");
+ if (File.Exists(candidate)) { eventsPath = candidate; break; }
+ }
+ }
+ }
+ if (File.Exists(eventsPath))
+ try { fileSize = new FileInfo(eventsPath).Length; } catch { }
+ }
+
+ var summary = $"{scenarioName} ({role})";
+ var runTag = runIndex > 0 ? $" #{runIndex}" : "";
+ var cwd = workDir ?? skillPath;
+
+ results.Add(new BrowserSession(
+ Id: id,
+ Summary: summary + runTag,
+ Cwd: cwd,
+ UpdatedAt: updatedAt,
+ EventsPath: eventsPath,
+ FileSize: fileSize,
+ Branch: model,
+ Repository: skillName,
+ DbType: SessionDbType.SkillValidator,
+ SkillName: skillName,
+ ScenarioName: scenarioName,
+ Role: role,
+ Model: model,
+ Status: status,
+ Prompt: prompt,
+ MetricsJson: metricsJson,
+ JudgeJson: judgeJson,
+ PairwiseJson: pairwiseJson));
+ }
+ return results;
+ }
+ catch
+ {
+ return null;
}
}
@@ -87,15 +236,16 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
return null;
}
- List<(string id, string summary, string cwd, DateTime updatedAt, string eventsPath, long fileSize, string branch, string repository)> allSessions = [];
+ List allSessions = [];
var sessionsLock = new System.Threading.Lock();
bool scanComplete = false;
int lastRenderedCount = -1;
+ bool isSkillDb = false;
// Background scan thread โ try DB first, fall back to file scan
var scanThread = new Thread(() =>
{
- // Try loading Copilot sessions from SQLite DB (fast path)
+ // Try loading sessions from SQLite DB (fast path)
var dbSessions = LoadSessionsFromDb(sessionStateDir!, dbPathOverride);
string? dbPath = null;
HashSet knownSessionIds = [];
@@ -105,15 +255,16 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
{
// Record the DB path for polling
dbPath = dbPathOverride ?? Path.Combine(Path.GetDirectoryName(sessionStateDir!)!, "session-store.db");
+ isSkillDb = _currentDbType == SessionDbType.SkillValidator;
lock (sessionsLock)
{
allSessions.AddRange(dbSessions);
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
foreach (var s in dbSessions)
{
- knownSessionIds.Add(s.id);
- if (s.updatedAt > lastUpdatedAt) lastUpdatedAt = s.updatedAt;
+ knownSessionIds.Add(s.Id);
+ if (s.UpdatedAt > lastUpdatedAt) lastUpdatedAt = s.UpdatedAt;
}
}
}
@@ -153,15 +304,15 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
lock (sessionsLock)
{
- allSessions.Add((id, summary, cwd, updatedAt, eventsPath, fileSize, "", ""));
+ allSessions.Add(new BrowserSession(id, summary, cwd, updatedAt, eventsPath, fileSize, "", ""));
knownSessionIds.Add(id);
if (allSessions.Count % 50 == 0)
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
}
}
lock (sessionsLock)
{
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
}
}
@@ -202,9 +353,9 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
long fileSize = new FileInfo(jsonlFile).Length;
lock (sessionsLock)
{
- allSessions.Add((claudeId, claudeSummary, claudeCwd, claudeUpdatedAt, jsonlFile, fileSize, "", ""));
+ allSessions.Add(new BrowserSession(claudeId, claudeSummary, claudeCwd, claudeUpdatedAt, jsonlFile, fileSize, "", ""));
if (allSessions.Count % 50 == 0)
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
}
}
catch { continue; }
@@ -213,7 +364,7 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
}
lock (sessionsLock)
{
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
}
}
scanComplete = true;
@@ -234,7 +385,7 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
cmd.Parameters.AddWithValue("@lastUpdated", lastUpdatedAt.ToString("o"));
using var reader = cmd.ExecuteReader();
- var newSessions = new List<(string id, string summary, string cwd, DateTime updatedAt, string eventsPath, long fileSize, string branch, string repository)>();
+ var newSessions = new List();
while (reader.Read())
{
@@ -256,7 +407,7 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
else
continue;
- newSessions.Add((id, summary, cwd, updatedAt, eventsPath, fileSize, branch, repository));
+ newSessions.Add(new BrowserSession(id, summary, cwd, updatedAt, eventsPath, fileSize, branch, repository));
knownSessionIds.Add(id);
if (updatedAt > lastUpdatedAt) lastUpdatedAt = updatedAt;
}
@@ -266,7 +417,7 @@ class SessionBrowser(ContentRenderer cr, DataParsers dataParsers, string? sessio
lock (sessionsLock)
{
allSessions.AddRange(newSessions);
- allSessions.Sort((a, b) => b.updatedAt.CompareTo(a.updatedAt));
+ allSessions.Sort((a, b) => b.UpdatedAt.CompareTo(a.UpdatedAt));
}
}
}
@@ -299,7 +450,7 @@ void RebuildFiltered()
if (searchFilter.Length > 0)
{
var s = allSessions[i];
- var text = $"{s.summary} {s.cwd} {s.id} {s.branch} {s.repository}";
+ var text = $"{s.Summary} {s.Cwd} {s.Id} {s.Branch} {s.Repository} {s.SkillName} {s.ScenarioName} {s.Role} {s.Model}";
if (!text.Contains(searchFilter, StringComparison.OrdinalIgnoreCase))
continue;
}
@@ -327,19 +478,22 @@ void ClampCursor()
void LoadPreview()
{
if (filtered.Count == 0 || cursorIdx >= filtered.Count) { previewLines.Clear(); return; }
- string eventsPath;
- string id;
- lock (sessionsLock)
- {
- var s = allSessions[filtered[cursorIdx]];
- eventsPath = s.eventsPath;
- id = s.id;
- }
- if (id == previewSessionId) return;
- previewSessionId = id;
+ BrowserSession sess;
+ lock (sessionsLock) { sess = allSessions[filtered[cursorIdx]]; }
+ if (sess.Id == previewSessionId) return;
+ previewSessionId = sess.Id;
previewScroll = 0;
try
{
+ if (sess.DbType == SessionDbType.SkillValidator)
+ {
+ // Show eval metadata as preview for skill-validator sessions
+ previewLines = RenderSkillPreview(sess);
+ previewScroll = 0;
+ return;
+ }
+
+ var eventsPath = sess.EventsPath;
var data = IsClaudeFormat(eventsPath) ? dataParsers.ParseClaudeData(eventsPath) : dataParsers.ParseJsonlData(eventsPath);
if (data == null) { previewLines = ["", " (unable to load preview)"]; return; }
if (data.Turns.Count > 50)
@@ -354,6 +508,82 @@ void LoadPreview()
}
}
+ List RenderSkillPreview(BrowserSession s)
+ {
+ var lines = new List
+ {
+ "",
+ $" [bold]Skill:[/] {Markup.Escape(s.SkillName ?? "")}",
+ $" [bold]Scenario:[/] {Markup.Escape(s.ScenarioName ?? "")}",
+ $" [bold]Role:[/] {Markup.Escape(s.Role ?? "")}",
+ $" [bold]Model:[/] {Markup.Escape(s.Model ?? "")}",
+ $" [bold]Status:[/] {Markup.Escape(s.Status ?? "")}",
+ ""
+ };
+
+ if (!string.IsNullOrEmpty(s.Prompt))
+ {
+ lines.Add(" [bold]Prompt:[/]");
+ var promptLines = s.Prompt.Split('\n');
+ foreach (var pl in promptLines.Take(10))
+ lines.Add($" {Markup.Escape(pl.TrimEnd())}");
+ if (promptLines.Length > 10)
+ lines.Add($" [dim]... ({promptLines.Length - 10} more lines)[/]");
+ lines.Add("");
+ }
+
+ if (!string.IsNullOrEmpty(s.MetricsJson))
+ {
+ lines.Add(" [bold]Metrics:[/]");
+ try
+ {
+ var doc = JsonDocument.Parse(s.MetricsJson);
+ foreach (var prop in doc.RootElement.EnumerateObject().Take(10))
+ lines.Add($" {Markup.Escape(prop.Name)}: {Markup.Escape(prop.Value.ToString())}");
+ }
+ catch { lines.Add($" {Markup.Escape(s.MetricsJson[..Math.Min(200, s.MetricsJson.Length)])}"); }
+ lines.Add("");
+ }
+
+ if (!string.IsNullOrEmpty(s.JudgeJson))
+ {
+ lines.Add(" [bold]Judge Result:[/]");
+ try
+ {
+ var doc = JsonDocument.Parse(s.JudgeJson);
+ foreach (var prop in doc.RootElement.EnumerateObject().Take(10))
+ lines.Add($" {Markup.Escape(prop.Name)}: {Markup.Escape(prop.Value.ToString())}");
+ }
+ catch { lines.Add($" {Markup.Escape(s.JudgeJson[..Math.Min(200, s.JudgeJson.Length)])}"); }
+ lines.Add("");
+ }
+
+ if (!string.IsNullOrEmpty(s.PairwiseJson))
+ {
+ lines.Add(" [bold]Pairwise:[/]");
+ try
+ {
+ var doc = JsonDocument.Parse(s.PairwiseJson);
+ foreach (var prop in doc.RootElement.EnumerateObject().Take(10))
+ lines.Add($" {Markup.Escape(prop.Name)}: {Markup.Escape(prop.Value.ToString())}");
+ }
+ catch { lines.Add($" {Markup.Escape(s.PairwiseJson[..Math.Min(200, s.PairwiseJson.Length)])}"); }
+ lines.Add("");
+ }
+
+ if (!string.IsNullOrEmpty(s.EventsPath) && File.Exists(s.EventsPath))
+ {
+ lines.Add($" [dim]Events: {Markup.Escape(s.EventsPath)}[/]");
+ lines.Add($" [dim]Press Enter to view transcript[/]");
+ }
+ else
+ {
+ lines.Add(" [dim]No events.jsonl available[/]");
+ }
+
+ return lines;
+ }
+
void Render()
{
int w;
@@ -378,11 +608,12 @@ void Render()
lock (sessionsLock)
{
var cs = allSessions[filtered[cursorIdx]];
- var updated = cs.updatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
- cursorInfo = $" | {cs.id} {updated}";
+ var updated = cs.UpdatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
+ cursorInfo = $" | {cs.Id} {updated}";
}
}
- var headerBase = $" ๐ Sessions โ {count} sessions{loadingStatus}{filterStatus}";
+ var headerLabel = isSkillDb ? "๐งช Skill Eval" : "๐ Sessions";
+ var headerBase = $" {headerLabel} โ {count} sessions{loadingStatus}{filterStatus}";
var headerText = headerBase + cursorInfo;
int headerVis = VisibleWidth(headerText);
var hdrPad = headerVis < w ? new string(' ', w - headerVis) : "";
@@ -402,19 +633,34 @@ void Render()
// Left side: session list
if (vi < filtered.Count)
{
- string id, summary, cwd, eventsPath, branch, repository;
- DateTime updatedAt;
- long fileSize;
+ BrowserSession sess;
lock (sessionsLock)
{
var si = filtered[vi];
- (id, summary, cwd, updatedAt, eventsPath, fileSize, branch, repository) = allSessions[si];
+ sess = allSessions[si];
+ }
+ var age = FormatAge(DateTime.UtcNow - sess.UpdatedAt);
+ var size = FormatFileSize(sess.FileSize);
+ string icon;
+ if (sess.DbType == SessionDbType.SkillValidator)
+ {
+ var statusIcon = sess.Status switch
+ {
+ "completed" => "โ
",
+ "timed_out" => "โฑ๏ธ",
+ "running" => "๐",
+ _ => "๐งช"
+ };
+ icon = statusIcon;
}
- var age = FormatAge(DateTime.UtcNow - updatedAt);
- var size = FormatFileSize(fileSize);
- var icon = eventsPath.Contains(".claude") ? "๐ด" : "๐ค";
- var branchTag = !string.IsNullOrEmpty(branch) ? $" [{branch}]" : "";
- var display = !string.IsNullOrEmpty(summary) ? summary.ReplaceLineEndings(" ") : cwd;
+ else
+ icon = sess.EventsPath.Contains(".claude") ? "๐ด" : "๐ค";
+ var branchTag = !string.IsNullOrEmpty(sess.Branch) ? $" [{sess.Branch}]" : "";
+ string display;
+ if (sess.DbType == SessionDbType.SkillValidator)
+ display = !string.IsNullOrEmpty(sess.Summary) ? $"{sess.Repository}: {sess.Summary}" : sess.Cwd;
+ else
+ display = !string.IsNullOrEmpty(sess.Summary) ? sess.Summary.ReplaceLineEndings(" ") : sess.Cwd;
int maxDisplay = Math.Max(10, listWidth - 19 - VisibleWidth(branchTag));
if (VisibleWidth(display) > maxDisplay) display = TruncateToWidth(display, maxDisplay - 3) + "...";
display += branchTag;
@@ -611,7 +857,15 @@ void Render()
if (filtered.Count > 0 && cursorIdx < filtered.Count)
{
string path;
- lock (sessionsLock) { path = allSessions[filtered[cursorIdx]].eventsPath; }
+ lock (sessionsLock) { path = allSessions[filtered[cursorIdx]].EventsPath; }
+ if (string.IsNullOrEmpty(path) || !File.Exists(path))
+ {
+ // No events file โ toggle preview instead
+ if (!showPreview) { showPreview = true; previewSessionId = null; }
+ AnsiConsole.Clear();
+ Render();
+ break;
+ }
Console.CursorVisible = true;
AnsiConsole.Clear();
return path;
@@ -694,7 +948,7 @@ void Render()
if (filtered.Count > 0 && cursorIdx >= 0 && cursorIdx < filtered.Count)
{
string rPath;
- lock (sessionsLock) { rPath = allSessions[filtered[cursorIdx]].eventsPath; }
+ lock (sessionsLock) { rPath = allSessions[filtered[cursorIdx]].EventsPath; }
Console.CursorVisible = true;
AnsiConsole.Clear();
LaunchResume(rPath);
@@ -741,8 +995,17 @@ public void LaunchResume(string path)
}
else
{
- command = "copilot";
- args = $"--resume \"{sessionId}\"";
+ // Copilot CLI can be installed standalone ("copilot") or as a gh extension ("gh copilot")
+ if (CanRun("copilot"))
+ {
+ command = "copilot";
+ args = $"--resume \"{sessionId}\"";
+ }
+ else
+ {
+ command = "gh";
+ args = $"copilot --resume \"{sessionId}\"";
+ }
}
Console.WriteLine($"Resuming session with: {command} {args}");
@@ -762,7 +1025,27 @@ public void LaunchResume(string path)
catch (Exception ex)
{
Console.Error.WriteLine($"Error launching {command}: {ex.Message}");
- Console.Error.WriteLine($"Make sure '{command}' is installed and available in your PATH.");
+ Console.Error.WriteLine("Make sure 'copilot' or 'gh copilot' is installed and available in your PATH.");
+ }
+ }
+
+ static bool CanRun(string command)
+ {
+ try
+ {
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = command,
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using var proc = System.Diagnostics.Process.Start(psi);
+ proc?.WaitForExit(3000);
+ return proc?.ExitCode == 0;
}
+ catch { return false; }
}
}
diff --git a/TextUtils.cs b/TextUtils.cs
index fe17a18..d5687bc 100644
--- a/TextUtils.cs
+++ b/TextUtils.cs
@@ -288,7 +288,7 @@ public static string ExtractContentString(JsonElement el)
public static string SafeGetString(JsonElement el, string prop)
{
- if (el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String)
+ if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String)
return v.GetString() ?? "";
return "";
}
diff --git a/replay.cs b/replay.cs
index ad892b0..e7139f9 100644
--- a/replay.cs
+++ b/replay.cs
@@ -595,7 +595,7 @@ replay stats [options]
(no args) Browse recent Copilot CLI sessions
Options:
- --db Browse sessions from an external session-store.db file
+ --db Browse sessions from a session-store.db or skill-validator sessions.db
--tail Show only the last N conversation turns
--expand-tools Show tool arguments, results, and thinking/reasoning
--full Don't truncate tool output (use with --expand-tools)
diff --git a/tests/DbPathTests.cs b/tests/DbPathTests.cs
index d9d9e13..ca3addf 100644
--- a/tests/DbPathTests.cs
+++ b/tests/DbPathTests.cs
@@ -54,7 +54,7 @@ public void HelpText_IncludesDbFlag()
var (stdout, stderr) = RunReplayWithArgs("--help");
Assert.Contains("--db", stdout);
- Assert.Contains("Browse sessions from an external session-store.db file", stdout);
+ Assert.Contains("Browse sessions from a session-store.db or skill-validator sessions.db", stdout);
}
// Helper to create an empty DB file
@@ -74,7 +74,7 @@ private string CreateEmptyDbFile()
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "dotnet",
- Arguments = $"run --project {ReplayCs} -- {args}",
+ Arguments = $"run -v q --project {ReplayCs} -- {args}",
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // Need to redirect input too for proper TTY detection
diff --git a/tests/EdgeCaseTests.cs b/tests/EdgeCaseTests.cs
index 9e1f549..6196908 100644
--- a/tests/EdgeCaseTests.cs
+++ b/tests/EdgeCaseTests.cs
@@ -472,7 +472,7 @@ private string RunReplayWithArgs(string args)
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "dotnet",
- Arguments = $"run --project {ReplayCs} -- {args}",
+ Arguments = $"run -v q --project {ReplayCs} -- {args}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
diff --git a/tests/JsonOutputTests.cs b/tests/JsonOutputTests.cs
index 46988c0..78e1f89 100644
--- a/tests/JsonOutputTests.cs
+++ b/tests/JsonOutputTests.cs
@@ -316,7 +316,7 @@ private string RunReplayWithArgs(string args)
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "dotnet",
- Arguments = $"run --project {ReplayCs} -- {args}",
+ Arguments = $"run -v q --project {ReplayCs} -- {args}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
diff --git a/tests/SummaryOutputTests.cs b/tests/SummaryOutputTests.cs
index f088678..0a71b40 100644
--- a/tests/SummaryOutputTests.cs
+++ b/tests/SummaryOutputTests.cs
@@ -429,7 +429,7 @@ private string RunReplayWithArgs(string args)
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "dotnet",
- Arguments = $"run --project {ReplayCs} -- {args}",
+ Arguments = $"run -v q --project {ReplayCs} -- {args}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,