const std = @import("std"); const session_display_metadata = @import("session_display_metadata.zig"); const session_store = @import("session_store.zig"); const text_utils = @import("../shared/text_utils.zig"); pub const Scope = enum { current_workspace, all_workspaces, }; pub const LoadState = enum { loading, ready, failed, }; pub const ResumeFailure = enum { open_elsewhere, being_updated, unavailable, }; pub fn filteredCount(summaries: []const session_store.SessionSummary, query: []const u8) usize { var count: usize = 0; for (summaries) |summary| { if (matchesQuery(summary, query)) count += 1; } return count; } pub fn summaryAt( summaries: []const session_store.SessionSummary, query: []const u8, display_index: usize, ) ?*const session_store.SessionSummary { var current: usize = 0; for (summaries) |*summary| { if (!matchesQuery(summary.*, query)) continue; if (current == display_index) return summary; current += 1; } return null; } pub fn displayTitle(summary: session_store.SessionSummary) []const u8 { return summary.title orelse session_display_metadata.fallback_title; } pub fn workspacePath(summary: session_store.SessionSummary) []const u8 { return summary.workspace_root orelse summary.origin_workspace_root orelse "(unknown workspace)"; } pub fn relativeActivityAge(buf: []u8, updated_at_ms: i64, now_ms: i64) []const u8 { const delta_ms: i64 = @max(0, now_ms - updated_at_ms); if (delta_ms < std.time.ms_per_min) return "just now"; if (delta_ms < std.time.ms_per_hour) { return std.fmt.bufPrint(buf, "{d}m ago", .{@divFloor(delta_ms, std.time.ms_per_min)}) catch "recently"; } if (delta_ms < std.time.ms_per_day) { return std.fmt.bufPrint(buf, "{d}h ago", .{@divFloor(delta_ms, std.time.ms_per_hour)}) catch "recently"; } return std.fmt.bufPrint(buf, "{d}d ago", .{@divFloor(delta_ms, std.time.ms_per_day)}) catch "recently"; } // Same buckets as relativeActivityAge without the " ago" suffix, for tight // single-line layouts: "now", "5m", "2h", "1d". pub fn relativeActivityAgeCompact(buf: []u8, updated_at_ms: i64, now_ms: i64) []const u8 { const delta_ms: i64 = @max(0, now_ms - updated_at_ms); if (delta_ms < std.time.ms_per_min) return "now"; if (delta_ms < std.time.ms_per_hour) { return std.fmt.bufPrint(buf, "{d}m", .{@divFloor(delta_ms, std.time.ms_per_min)}) catch "recently"; } if (delta_ms < std.time.ms_per_day) { return std.fmt.bufPrint(buf, "{d}h", .{@divFloor(delta_ms, std.time.ms_per_hour)}) catch "recently"; } return std.fmt.bufPrint(buf, "{d}d", .{@divFloor(delta_ms, std.time.ms_per_day)}) catch "recently"; } fn matchesQuery(summary: session_store.SessionSummary, query: []const u8) bool { const query_text = std.mem.trim(u8, query, " \t\r\n"); if (query_text.len == 0) return true; if (text_utils.containsIgnoreCase(displayTitle(summary), query_text)) return true; if (text_utils.containsIgnoreCase(workspacePath(summary), query_text)) return true; if (summary.preview) |preview| { if (text_utils.containsIgnoreCase(preview, query_text)) return true; } return false; } test "session catalog filters titles previews and workspace paths" { const summaries = [_]session_store.SessionSummary{ .{ .id = @constCast("one"), .workspace_root = @constCast("/worktrees/alpha"), .title = @constCast("Fix renderer"), .preview = @constCast("inspect footer state"), .created_at_ms = 1, .updated_at_ms = 2, .conversation_language = .literal("en"), .history_len = 4, }, .{ .id = @constCast("two"), .workspace_root = @constCast("/worktrees/beta"), .title = @constCast("Review sessions"), .created_at_ms = 1, .updated_at_ms = 3, .conversation_language = .literal("en"), .history_len = 2, }, }; try std.testing.expectEqual(@as(usize, 2), filteredCount(&summaries, "")); try std.testing.expectEqual(@as(usize, 1), filteredCount(&summaries, "RENDER")); try std.testing.expectEqualStrings("two", summaryAt(&summaries, "beta", 0).?.id); try std.testing.expectEqualStrings("one", summaryAt(&summaries, "footer", 0).?.id); try std.testing.expect(summaryAt(&summaries, "missing", 0) == null); } test "session catalog activity age uses the last update" { var buf: [32]u8 = undefined; try std.testing.expectEqualStrings("just now", relativeActivityAge(&buf, 1_000, 1_000)); try std.testing.expectEqualStrings("3m ago", relativeActivityAge(&buf, 1_000, 1_000 + 3 * std.time.ms_per_min)); try std.testing.expectEqualStrings("2h ago", relativeActivityAge(&buf, 1_000, 1_000 + 2 * std.time.ms_per_hour)); try std.testing.expectEqualStrings("3d ago", relativeActivityAge(&buf, 1_000, 1_000 + 3 * std.time.ms_per_day)); } test "session catalog compact activity age drops the ago suffix" { var buf: [32]u8 = undefined; try std.testing.expectEqualStrings("now", relativeActivityAgeCompact(&buf, 1_000, 1_000)); try std.testing.expectEqualStrings("3m", relativeActivityAgeCompact(&buf, 1_000, 1_000 + 3 * std.time.ms_per_min)); try std.testing.expectEqualStrings("2h", relativeActivityAgeCompact(&buf, 1_000, 1_000 + 2 * std.time.ms_per_hour)); try std.testing.expectEqualStrings("3d", relativeActivityAgeCompact(&buf, 1_000, 1_000 + 3 * std.time.ms_per_day)); }