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
16 changes: 13 additions & 3 deletions cmd/agentsview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ func main() {
func printUsage() {
fmt.Printf(`agentsview %s - local web viewer for AI agent sessions

Syncs Claude Code, Codex, Copilot CLI, Gemini CLI, and OpenCode session
data into SQLite, serves an analytics dashboard and session browser via
a local web UI.
Syncs Claude Code, Codex, Copilot CLI, Gemini CLI, OpenCode, and Cursor
session data into SQLite, serves an analytics dashboard and session
browser via a local web UI.

Usage:
agentsview [flags] Start the server (default command)
Expand Down Expand Up @@ -98,6 +98,7 @@ Environment variables:
COPILOT_DIR Copilot CLI directory
GEMINI_DIR Gemini CLI directory
OPENCODE_DIR OpenCode data directory
CURSOR_PROJECTS_DIR Cursor projects directory
AGENT_VIEWER_DATA_DIR Data directory (database, config)

Multiple directories:
Expand Down Expand Up @@ -159,6 +160,7 @@ func runServe(args []string) {
cfg.ResolveCopilotDirs(),
cfg.ResolveGeminiDirs(),
cfg.ResolveOpenCodeDirs(),
cfg.CursorProjectsDir,
"local",
)

Expand Down Expand Up @@ -364,6 +366,14 @@ func startFileWatcher(
roots = append(roots, watchRoot{d, geminiTmp})
}
}
if cfg.CursorProjectsDir != "" {
if _, err := os.Stat(cfg.CursorProjectsDir); err == nil {
roots = append(roots, watchRoot{
cfg.CursorProjectsDir,
cfg.CursorProjectsDir,
})
}
}

var totalWatched int
for _, r := range roots {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
sessions.initFromParams(params);
sessions.load();
sessions.loadProjects();
sessions.loadAgents();
});
});

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
--accent-purple: #7c3aed;
--accent-amber: #d97706;
--accent-green: #059669;
--accent-black: #2d2d2d;
--accent-red: #dc2626;
--user-bg: #eef2ff;
--assistant-bg: #faf9ff;
Expand Down Expand Up @@ -61,6 +62,7 @@
--accent-purple: #a78bfa;
--accent-amber: #fbbf24;
--accent-green: #34d399;
--accent-black: #b0b0b0;
--accent-red: #f87171;
--user-bg: #111827;
--assistant-bg: #141220;
Expand Down
106 changes: 28 additions & 78 deletions frontend/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
SearchResponse,
ProjectsResponse,
MachinesResponse,
AgentsResponse,
Stats,
VersionInfo,
SyncStatus,
Expand Down Expand Up @@ -43,38 +44,22 @@ export class ApiError extends Error {
}
}

function apiErrorMessage(
status: number,
body: string,
): string {
function apiErrorMessage(status: number, body: string): string {
return body.trim() || `API ${status}`;
}

async function fetchJSON<T>(
path: string,
init?: RequestInit,
): Promise<T> {
async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, init);
if (!res.ok) {
const body = await res.text();
throw new ApiError(
res.status,
apiErrorMessage(res.status, body),
);
throw new ApiError(res.status, apiErrorMessage(res.status, body));
}
return res.json() as Promise<T>;
}

type QueryValue =
| string
| number
| boolean
| undefined
| null;
type QueryValue = string | number | boolean | undefined | null;

function buildQuery(
params: Record<string, QueryValue>,
): string {
function buildQuery(params: Record<string, QueryValue>): string {
const q = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") {
Expand Down Expand Up @@ -109,10 +94,7 @@ export function listSessions(
return fetchJSON(`/sessions${buildQuery({ ...params })}`);
}

export function getSession(
id: string,
init?: RequestInit,
): Promise<Session> {
export function getSession(id: string, init?: RequestInit): Promise<Session> {
return fetchJSON(`/sessions/${id}`, init);
}

Expand Down Expand Up @@ -163,10 +145,7 @@ export function search(
if (!query) {
throw new Error("search query must not be empty");
}
return fetchJSON(
`/search${buildQuery({ q: query, ...params })}`,
init,
);
return fetchJSON(`/search${buildQuery({ q: query, ...params })}`, init);
}

/* Metadata */
Expand All @@ -179,6 +158,10 @@ export function getMachines(): Promise<MachinesResponse> {
return fetchJSON("/machines");
}

export function getAgents(): Promise<AgentsResponse> {
return fetchJSON("/agents");
}

export function getStats(): Promise<Stats> {
return fetchJSON("/stats");
}
Expand Down Expand Up @@ -318,9 +301,7 @@ export function watchSession(
sessionId: string,
onUpdate: () => void,
): EventSource {
const es = new EventSource(
`${BASE}/sessions/${sessionId}/watch`,
);
const es = new EventSource(`${BASE}/sessions/${sessionId}/watch`);

es.addEventListener("session_updated", () => {
onUpdate();
Expand All @@ -340,9 +321,7 @@ export function getExportUrl(sessionId: string): string {

/* Publish / GitHub config */

export function publishSession(
sessionId: string,
): Promise<PublishResponse> {
export function publishSession(sessionId: string): Promise<PublishResponse> {
return fetchJSON(`/sessions/${sessionId}/publish`, {
method: "POST",
});
Expand Down Expand Up @@ -380,79 +359,61 @@ export interface AnalyticsParams {
export function getAnalyticsSummary(
params: AnalyticsParams,
): Promise<AnalyticsSummary> {
return fetchJSON(
`/analytics/summary${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/summary${buildQuery({ ...params })}`);
}

export function getAnalyticsActivity(
params: AnalyticsParams & {
granularity?: Granularity;
},
): Promise<ActivityResponse> {
return fetchJSON(
`/analytics/activity${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/activity${buildQuery({ ...params })}`);
}

export function getAnalyticsHeatmap(
params: AnalyticsParams & {
metric?: HeatmapMetric;
},
): Promise<HeatmapResponse> {
return fetchJSON(
`/analytics/heatmap${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/heatmap${buildQuery({ ...params })}`);
}

export function getAnalyticsProjects(
params: AnalyticsParams,
): Promise<ProjectsAnalyticsResponse> {
return fetchJSON(
`/analytics/projects${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/projects${buildQuery({ ...params })}`);
}

export function getAnalyticsHourOfWeek(
params: AnalyticsParams,
): Promise<HourOfWeekResponse> {
return fetchJSON(
`/analytics/hour-of-week${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/hour-of-week${buildQuery({ ...params })}`);
}

export function getAnalyticsSessionShape(
params: AnalyticsParams,
): Promise<SessionShapeResponse> {
return fetchJSON(
`/analytics/sessions${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/sessions${buildQuery({ ...params })}`);
}

export function getAnalyticsVelocity(
params: AnalyticsParams,
): Promise<VelocityResponse> {
return fetchJSON(
`/analytics/velocity${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/velocity${buildQuery({ ...params })}`);
}

export function getAnalyticsTools(
params: AnalyticsParams,
): Promise<ToolsAnalyticsResponse> {
return fetchJSON(
`/analytics/tools${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/tools${buildQuery({ ...params })}`);
}

export function getAnalyticsTopSessions(
params: AnalyticsParams & {
metric?: TopSessionsMetric;
},
): Promise<TopSessionsResponse> {
return fetchJSON(
`/analytics/top-sessions${buildQuery({ ...params })}`,
);
return fetchJSON(`/analytics/top-sessions${buildQuery({ ...params })}`);
}

/* Insights */
Expand All @@ -465,9 +426,7 @@ export interface ListInsightsParams {
export function listInsights(
params: ListInsightsParams = {},
): Promise<InsightsResponse> {
return fetchJSON(
`/insights${buildQuery({ ...params })}`,
);
return fetchJSON(`/insights${buildQuery({ ...params })}`);
}

export function getInsight(id: number): Promise<Insight> {
Expand All @@ -480,10 +439,7 @@ export async function deleteInsight(id: number): Promise<void> {
});
if (!res.ok) {
const body = await res.text();
throw new ApiError(
res.status,
apiErrorMessage(res.status, body),
);
throw new ApiError(res.status, apiErrorMessage(res.status, body));
}
}

Expand All @@ -507,9 +463,7 @@ export function generateInsight(
});

if (!res.ok || !res.body) {
throw new Error(
`Generate request failed: ${res.status}`,
);
throw new Error(`Generate request failed: ${res.status}`);
}

const reader = res.body.getReader();
Expand All @@ -523,9 +477,7 @@ export function generateInsight(
buf += decoder.decode(value, { stream: true });
buf = buf.replaceAll("\r\n", "\n");

const parsed = processInsightFrames(
buf, onStatus,
);
const parsed = processInsightFrames(buf, onStatus);
if (parsed) {
result = parsed;
reader.cancel();
Expand All @@ -543,9 +495,7 @@ export function generateInsight(
}

if (!result) {
throw new Error(
"Generate stream ended without done event",
);
throw new Error("Generate stream ended without done event");
}

return result;
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/lib/api/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ export interface Message {
/** Matches Go MinimapEntry struct */
export type MinimapEntry = Pick<
Message,
| "ordinal"
| "role"
| "content_length"
| "has_thinking"
| "has_tool_use"
"ordinal" | "role" | "content_length" | "has_thinking" | "has_tool_use"
>;

/** Matches Go SearchResult struct in internal/db/search.go */
Expand Down Expand Up @@ -115,3 +111,13 @@ export interface ProjectsResponse {
export interface MachinesResponse {
machines: string[];
}

/** Matches Go AgentInfo struct */
export interface AgentInfo {
name: string;
session_count: number;
}

export interface AgentsResponse {
agents: AgentInfo[];
}
1 change: 1 addition & 0 deletions frontend/src/lib/components/analytics/ActiveFilters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@
</button>
{/if}


{#if hasTime}
<button
class="filter-chip"
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/lib/components/layout/AppHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
const isMac = navigator.platform.toUpperCase().includes("MAC");
const modKey = isMac ? "Cmd" : "Ctrl";

function handleAgentChange(e: Event) {
const select = e.target as HTMLSelectElement;
sessions.setAgentFilter(select.value);
}


function handleExport() {
if (sessions.activeSessionId) {
window.open(
Expand Down Expand Up @@ -49,6 +55,19 @@
onselect={(v) => sessions.setProjectFilter(v)}
/>

<select
class="project-select"
value={sessions.filters.agent}
onchange={handleAgentChange}
>
<option value="">All Agents</option>
{#each sessions.agents as agent}
<option value={agent.name}>
{agent.name} ({agent.session_count})
</option>
{/each}
</select>

<button
class="nav-btn"
class:active={router.route === "sessions"}
Expand Down
Loading