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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ sessions/
html/
.superset/
.github/hooks/
.superpowers/
126 changes: 126 additions & 0 deletions frontend/e2e/activity-minimap.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { test, expect } from "@playwright/test";
import { SessionsPage } from "./pages/sessions-page";

test.describe("Activity Minimap", () => {
let sp: SessionsPage;

test.beforeEach(async ({ page }) => {
sp = new SessionsPage(page);
await sp.goto();
await sp.selectSession(0);
});

test("minimap is hidden by default", async ({
page,
}) => {
await expect(
page.locator(".activity-minimap"),
).not.toBeVisible();
});

test("toggle button shows and hides minimap", async ({
page,
}) => {
await page.locator(".minimap-btn").click();
await expect(
page.locator(".activity-minimap"),
).toBeVisible();

await page.locator(".minimap-btn").click();
await expect(
page.locator(".activity-minimap"),
).not.toBeVisible();
});

test("minimap shows bars for sessions with timestamps", async ({
page,
}) => {
await page.locator(".minimap-btn").click();

await expect(
page.locator(".minimap-status"),
).not.toBeVisible({ timeout: 3000 });

const bars = page.locator(".minimap-bar");
const count = await bars.count();
expect(count).toBeGreaterThan(0);
});

test("clicking a bar scrolls the message list", async ({
page,
}) => {
await page.locator(".minimap-btn").click();

const clickableBars = page.locator(
"g.minimap-bar[role='button']",
);
await clickableBars.first().waitFor({ timeout: 3000 });

const barCount = await clickableBars.count();
expect(barCount).toBeGreaterThan(0);

const scrollBefore = await sp.scroller.evaluate(
(el) => el.scrollTop,
);

const lastBar = clickableBars.nth(barCount - 1);
await lastBar.click();

if (barCount > 1) {
await expect
.poll(
() =>
sp.scroller.evaluate((el) => el.scrollTop),
{ timeout: 3000 },
)
.not.toBe(scrollBefore);
}
});

test("active indicator moves after reopen without scroll", async ({
page,
}) => {
// Open minimap, wait for multiple bars.
await page.locator(".minimap-btn").click();
const bars = page.locator("g.minimap-bar[role='button']");
await bars.first().waitFor({ timeout: 3000 });

const barCount = await bars.count();
if (barCount < 2) {
// Can't test indicator movement with < 2 bars.
return;
}

// Record the x position of the active indicator.
const indicator = page.locator(".bar-indicator");
await expect(indicator).toBeVisible();
const xBefore = await indicator.evaluate(
(el) => el.getAttribute("x"),
);

// Close minimap, scroll to the bottom.
await page.locator(".minimap-btn").click();
await expect(
page.locator(".activity-minimap"),
).not.toBeVisible();

await sp.scroller.evaluate((el) => {
el.scrollTop = el.scrollHeight;
});

// Reopen — indicator should appear at a different
// position reflecting the new scroll location.
await page.locator(".minimap-btn").click();
await expect(indicator).toBeVisible({ timeout: 3000 });

await expect
.poll(
() =>
indicator.evaluate(
(el) => el.getAttribute("x"),
),
{ timeout: 3000 },
)
.not.toBe(xBefore);
});
});
13 changes: 13 additions & 0 deletions frontend/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import StatusBar from "./lib/components/layout/StatusBar.svelte";
import SessionList from "./lib/components/sidebar/SessionList.svelte";
import MessageList from "./lib/components/content/MessageList.svelte";
import ActivityMinimap from "./lib/components/content/ActivityMinimap.svelte";
import { sessionActivity } from "./lib/stores/sessionActivity.svelte.js";
import CommandPalette from "./lib/components/command-palette/CommandPalette.svelte";
import AboutModal from "./lib/components/modals/AboutModal.svelte";
import ShortcutsModal from "./lib/components/modals/ShortcutsModal.svelte";
Expand Down Expand Up @@ -84,9 +86,15 @@
messages.reload();
sessions.refreshActiveSession();
sessions.loadChildSessions(id);
if (ui.activityMinimapOpen) {
sessionActivity.reload(id);
} else {
sessionActivity.invalidate();
}
});
pins.loadForSession(id);
} else {
sessionActivity.clear();
messages.clear();
sessions.childSessions = new Map();
sync.unwatchSession();
Expand Down Expand Up @@ -380,6 +388,11 @@
session={session}
onBack={() => sessions.deselectSession()}
/>
{#if ui.activityMinimapOpen && sessions.activeSessionId}
<ActivityMinimap
sessionId={sessions.activeSessionId}
/>
{/if}
<MessageList bind:this={messageListRef} />
{:else}
<AnalyticsPage />
Expand Down
24 changes: 9 additions & 15 deletions frontend/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {
SessionPage,
Session,
MessagesResponse,
MinimapResponse,
SearchResponse,
ProjectsResponse,
MachinesResponse,
Expand Down Expand Up @@ -34,6 +33,7 @@ import type {
PinsResponse,
TrashResponse,
} from "./types.js";
import type { SessionActivityResponse } from "./types/session-activity.js";

const SERVER_URL_KEY = "agentsview-server-url";
const AUTH_TOKEN_KEY = "agentsview-auth-token";
Expand Down Expand Up @@ -171,6 +171,14 @@ export function getChildSessions(
return fetchJSON(`/sessions/${id}/children`, init);
}

export function getSessionActivity(
sessionId: string,
): Promise<SessionActivityResponse> {
return fetchJSON(
`/sessions/${sessionId}/activity`,
);
}

/* Messages */

export interface GetMessagesParams {
Expand All @@ -190,20 +198,6 @@ export function getMessages(
);
}

export interface GetMinimapParams {
from?: number;
max?: number;
}

export function getMinimap(
sessionId: string,
params: GetMinimapParams = {},
): Promise<MinimapResponse> {
return fetchJSON(
`/sessions/${sessionId}/minimap${buildQuery({ ...params })}`,
);
}

/* Search */

export function search(
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/lib/api/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,6 @@ export interface Message {
is_system: boolean;
}

/** Matches Go MinimapEntry struct */
export type MinimapEntry = Pick<
Message,
"ordinal" | "role" | "content_length" | "has_thinking" | "has_tool_use"
>;

/** Matches Go SearchResult struct in internal/db/search.go */
export interface SearchResult {
session_id: string;
Expand All @@ -104,11 +98,6 @@ export interface MessagesResponse {
count: number;
}

export interface MinimapResponse {
entries: MinimapEntry[];
count: number;
}

export interface SearchResponse {
query: string;
results: SearchResult[];
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export type * from "./sync.js";
export type * from "./analytics.js";
export type * from "./github.js";
export type * from "./insights.js";
export type * from "./session-activity.js";
13 changes: 13 additions & 0 deletions frontend/src/lib/api/types/session-activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface SessionActivityBucket {
start_time: string;
end_time: string;
user_count: number;
assistant_count: number;
first_ordinal: number | null;
}

export interface SessionActivityResponse {
buckets: SessionActivityBucket[];
interval_seconds: number;
total_messages: number;
}
Loading