Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
06e98a1
Generated with Hive: Build tabbed Context page and migrate Learn, Cal…
tomsmith8 Mar 20, 2026
0a092e4
Merge remote-tracking branch 'origin/master' into feature/cmmyspxxq00…
tomsmith8 Mar 20, 2026
861f409
Generated with Hive: Increase Playwright test timeout to fix navigati…
tomsmith8 Mar 20, 2026
e2030d8
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
6c6a9fa
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
a4343cf
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
4d6d6d0
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
9132110
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
e6d6e89
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
fe5255a
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
4f97828
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
2f577e2
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
bbfdf23
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 20, 2026
d57f993
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 21, 2026
d7e313f
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 22, 2026
ff40d83
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 22, 2026
2e31f2a
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 23, 2026
458dc3b
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
Rassl Mar 23, 2026
4050287
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 24, 2026
ba0d5d6
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 24, 2026
159a3eb
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 24, 2026
7c319f0
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 25, 2026
d6019ad
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 25, 2026
8df5dea
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 26, 2026
66c4dca
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 26, 2026
5df990a
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 26, 2026
efb37d3
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 26, 2026
b928c88
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 27, 2026
8ddfa28
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 27, 2026
24ea1c8
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 30, 2026
71c1cbc
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Mar 30, 2026
290381a
Generated with Hive: Fix TaskPage test to await ChatArea isLoading co…
tomsmith8 Apr 1, 2026
1d5ae50
Merge remote-tracking branch 'origin/master' into feature/cmmyspxxq00…
tomsmith8 Apr 1, 2026
9c7a544
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
80f6015
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
6204a17
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
9023f45
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
a282b1e
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
fd96a28
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
3f87e86
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
7d567bc
feat: sphinx login page
Rassl Apr 1, 2026
b78521f
Generated with Hive: Fix Agent Logs pagination resetting to page 1 af…
Evanfeenstra Apr 1, 2026
8e15365
Merge origin/master into feature branch - resolve conflicts keeping r…
tomsmith8 Apr 1, 2026
eccb925
Generated with Hive: Fix AgentLogsPage to debounce pagination reset o…
tomsmith8 Apr 1, 2026
97b607f
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
403ba53
Merge branch 'master' into feature/cmmyspxxq000djs040nea58ul-tabbed-c…
tomsmith8 Apr 1, 2026
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
19 changes: 7 additions & 12 deletions src/__tests__/e2e/specs/calls/calls-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,20 @@ test.describe('Calls Navigation', () => {
});

test('should navigate to calls page via sidebar', async ({ page }) => {
// Expand Context section first if needed
// Click the single Context sidebar link
const contextButton = page.locator('[data-testid="nav-context"]');
const callsLink = page.locator(selectors.navigation.callsLink);
await contextButton.click();
await page.waitForURL(/\/w\/.*\/context\/learn/, { timeout: 30000 });

const isCallsVisible = await callsLink.isVisible().catch(() => false);
if (!isCallsVisible) {
await contextButton.click();
await callsLink.waitFor({ state: 'visible', timeout: 5000 });
}

// Click the calls navigation link
await callsLink.click();
// Click the Calls tab
await page.locator('a[href*="/context/calls"]').first().click();

// Wait for URL to change to calls page
await page.waitForURL(/\/w\/.*\/calls/, { timeout: 10000 });
await page.waitForURL(/\/w\/.*\/context\/calls/, { timeout: 10000 });

// Verify we're on the calls page
await expect(page.locator(selectors.pageTitle.calls)).toBeVisible();
expect(page.url()).toContain('/calls');
expect(page.url()).toContain('/context/calls');
});

test('should display calls page title', async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/e2e/specs/capture-learn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test('Capture /learn documentation viewer UI', async ({ page }) => {
console.log('Workspace slug:', workspaceSlug);

// Navigate to learn page
await page.goto(`http://localhost:3000/w/${workspaceSlug}/learn`);
await page.goto(`http://localhost:3000/w/${workspaceSlug}/context/learn`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);

Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/e2e/specs/context/context-learn-message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test.describe('Context Learn Documentation Viewer', () => {
await contextLearnPage.navigateViaNavigation();

// Verify we're on the Context Learn page
await expect(page).toHaveURL(/\/w\/.*\/learn/, { timeout: 30000 });
await expect(page).toHaveURL(/\/w\/.*\/context\/learn/, { timeout: 30000 });

// Verify the page loaded (either docs or concepts section visible)
const isLoaded = await contextLearnPage.isLoaded();
Expand Down Expand Up @@ -91,7 +91,7 @@ test.describe('Context Learn Documentation Viewer', () => {
await contextLearnPage.navigateViaNavigation();

// Verify we're on Context Learn page
await expect(page).toHaveURL(/\/w\/.*\/learn/);
await expect(page).toHaveURL(/\/w\/.*\/context\/learn/);

// Verify docs section is present
await expect(page.locator(selectors.learn.docsSection)).toBeVisible();
Expand Down
23 changes: 9 additions & 14 deletions src/__tests__/e2e/support/page-objects/CallsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class CallsPage {
* Navigate to calls page for a specific workspace
*/
async goto(workspaceSlug: string): Promise<void> {
await this.page.goto(`http://localhost:3000/w/${workspaceSlug}/calls`);
await this.page.goto(`http://localhost:3000/w/${workspaceSlug}/context/calls`);
await this.waitForLoad();
}

Expand Down Expand Up @@ -56,19 +56,14 @@ export class CallsPage {
* Navigate to calls page via sidebar
*/
async navigateViaNavigation(): Promise<void> {
// First, expand Context section if it's not already expanded
const contextButton = this.page.locator(selectors.navigation.contextButton);
const callsLink = this.page.locator(selectors.navigation.callsLink);
// Click the single Context link in the sidebar — it redirects to /context/learn,
// then navigate directly to /context/calls via the tab bar
const contextLink = this.page.locator(selectors.navigation.contextButton);
await contextLink.click();
await this.page.waitForURL(/\/w\/.*\/context\/learn/, { timeout: 10000 });

// Check if calls link is visible, if not, click context to expand
const isCallsVisible = await callsLink.isVisible().catch(() => false);
if (!isCallsVisible) {
await contextButton.click();
// Wait for calls link to become visible after expanding
await callsLink.waitFor({ state: 'visible', timeout: 5000 });
}

await callsLink.click();
await this.page.waitForURL(/\/w\/.*\/calls/, { timeout: 10000 });
// Click the Calls tab
await this.page.locator('a[href*="/context/calls"]').first().click();
await this.page.waitForURL(/\/w\/.*\/context\/calls/, { timeout: 10000 });
}
}
33 changes: 10 additions & 23 deletions src/__tests__/e2e/support/page-objects/ContextLearnPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class ContextLearnPage {
* Navigate to Context Learn page for a specific workspace
*/
async goto(workspaceSlug: string): Promise<void> {
await this.page.goto(`http://localhost:3000/w/${workspaceSlug}/learn`);
await this.page.goto(`http://localhost:3000/w/${workspaceSlug}/context/learn`);
await this.waitForLoad();
}

Expand All @@ -32,35 +32,22 @@ export class ContextLearnPage {
* Navigate to Context Learn page via sidebar navigation
*/
async navigateViaNavigation(): Promise<void> {
// First expand the Context section if not already expanded
const contextButton = this.page.locator(selectors.navigation.contextButton);
const learnLink = this.page.locator(selectors.navigation.learnLink).first();

// Check if learn link is visible, if not, click Context to expand
const isLearnVisible = await learnLink.isVisible().catch(() => false);
if (!isLearnVisible) {
await contextButton.click();
await learnLink.waitFor({ state: 'visible', timeout: 15000 });
}
// Click the single Context link in the sidebar
const contextLink = this.page.locator(selectors.navigation.contextButton);

// Ensure page is fully loaded and network is idle before navigation
await this.page.waitForLoadState('networkidle', { timeout: 30000 }).catch(() => {
// If networkidle times out, fall back to domcontentloaded
return this.page.waitForLoadState('domcontentloaded');
});

// Wait for the link to be attached and stable
await learnLink.waitFor({ state: 'attached', timeout: 15000 });

// Small delay to ensure link is fully interactive (reduces race conditions)
await this.page.waitForTimeout(100);

// Wait for navigation to complete after clicking (using Promise.all for coordination)

await contextLink.waitFor({ state: 'attached', timeout: 15000 });

// Wait for navigation to complete after clicking
await Promise.all([
this.page.waitForURL(/\/w\/.*\/learn/, { timeout: 30000 }),
learnLink.click()
this.page.waitForURL(/\/w\/.*\/context\/learn/, { timeout: 30000 }),
contextLink.click()
]);

await this.waitForLoad();
}

Expand Down
145 changes: 43 additions & 102 deletions src/__tests__/unit/components/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,17 +238,7 @@ describe('Sidebar - Graph Explorer Context item', () => {
} as any);
});

// Helper: find the Context child "Graph" link (href = /context/graph)
// Note: there are 2 sidebars rendered (mobile + desktop) so use getAllBy
function findContextGraphLinks() {
return screen.queryAllByRole('link', { name: /^Graph$/i }).filter(
(el) => el.getAttribute('href') === '/w/test-workspace/context/graph',
);
}

it('shows Graph item under Context when canAdmin is true', async () => {
const user = userEvent.setup();

it('Context is a direct link to /w/[slug]/context (no expandable children in sidebar)', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true,
canWrite: true,
Expand All @@ -260,18 +250,15 @@ describe('Sidebar - Graph Explorer Context item', () => {

render(<Sidebar user={mockUser} />);

// Expand Context section (first sidebar instance = desktop)
const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
expect(findContextGraphLinks().length).toBeGreaterThan(0);
});
// nav-context should be an <a> link, not an expandable button
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
// Since it has no children it renders as a Link (asChild Button wrapping an <a>)
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});

it('Graph item href points to /context/graph', async () => {
const user = userEvent.setup();

it('Context link href points to /w/[slug]/context for admin users', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true,
canWrite: true,
Expand All @@ -283,19 +270,12 @@ describe('Sidebar - Graph Explorer Context item', () => {

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
const links = findContextGraphLinks();
expect(links.length).toBeGreaterThan(0);
expect(links[0]).toHaveAttribute('href', '/w/test-workspace/context/graph');
});
const contextItems = screen.getAllByTestId('nav-context');
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});

it('hides Graph item under Context when canAdmin is false', async () => {
const user = userEvent.setup();

it('Context link is present for non-admin users', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true,
canWrite: true,
Expand All @@ -307,21 +287,15 @@ describe('Sidebar - Graph Explorer Context item', () => {

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

// Wait for other context children to appear
await waitFor(() => {
expect(screen.getAllByTestId('nav-learn').length).toBeGreaterThan(0);
});

// Context/graph link must NOT be present
expect(findContextGraphLinks()).toHaveLength(0);
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
// No sub-items (Learn/Calls/Agent Logs) rendered in sidebar — they live in the context layout
expect(screen.queryByTestId('nav-learn')).not.toBeInTheDocument();
expect(screen.queryByTestId('nav-calls')).not.toBeInTheDocument();
expect(screen.queryByTestId('nav-agent-logs')).not.toBeInTheDocument();
});

it('still shows Learn, Calls, Agent Logs for non-admin users', async () => {
const user = userEvent.setup();

it('Context sidebar link is present for viewer role', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true,
canWrite: false,
Expand All @@ -333,14 +307,10 @@ describe('Sidebar - Graph Explorer Context item', () => {

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
expect(screen.getAllByTestId('nav-learn').length).toBeGreaterThan(0);
expect(screen.getAllByTestId('nav-calls').length).toBeGreaterThan(0);
expect(screen.getAllByTestId('nav-agent-logs').length).toBeGreaterThan(0);
});
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});
});

Expand Down Expand Up @@ -1026,85 +996,56 @@ describe('Sidebar - Graph Explorer Nav Item', () => {
} as any);
});

// Context child "Graph" is an <a> with href containing /context/graph
function contextGraphLinks() {
return screen.queryAllByRole('link').filter(
(el) => el.getAttribute('href') === '/w/test-workspace/context/graph',
);
}

it('should show Graph item under Context when user is admin', async () => {
const user = userEvent.setup();

it('should render Context as a direct sidebar link to /context for admin users', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true, canWrite: true, canAdmin: true, isOwner: false, hasAccess: true, role: 'ADMIN',
} as any);

render(<Sidebar user={mockUser} />);

// Both mobile + desktop sidebars render; click the first Context button
const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
expect(contextGraphLinks().length).toBeGreaterThan(0);
});
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});

it('should NOT show Graph item under Context when user is not admin', async () => {
const user = userEvent.setup();

it('should render Context as a direct sidebar link to /context for non-admin users', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true, canWrite: true, canAdmin: false, isOwner: false, hasAccess: true, role: 'DEVELOPER',
} as any);

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

// Wait for other children to render
await waitFor(() => {
expect(screen.getAllByTestId('nav-learn').length).toBeGreaterThan(0);
});

expect(contextGraphLinks()).toHaveLength(0);
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});

it('should show Graph item for OWNER role', async () => {
const user = userEvent.setup();

it('should render Context as a direct sidebar link for OWNER role', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true, canWrite: true, canAdmin: true, isOwner: true, hasAccess: true, role: 'OWNER',
} as any);

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
expect(contextGraphLinks().length).toBeGreaterThan(0);
});
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});

it('should NOT show Graph item for VIEWER role', async () => {
const user = userEvent.setup();

it('should render Context as a direct sidebar link for VIEWER role', () => {
vi.mocked(useWorkspaceAccessModule.useWorkspaceAccess).mockReturnValue({
canRead: true, canWrite: false, canAdmin: false, isOwner: false, hasAccess: true, role: 'VIEWER',
} as any);

render(<Sidebar user={mockUser} />);

const contextButtons = screen.getAllByTestId('nav-context');
await user.click(contextButtons[0]);

await waitFor(() => {
expect(screen.getAllByTestId('nav-learn').length).toBeGreaterThan(0);
});

expect(contextGraphLinks()).toHaveLength(0);
const contextItems = screen.getAllByTestId('nav-context');
expect(contextItems.length).toBeGreaterThan(0);
const contextLink = contextItems[0].querySelector('a');
expect(contextLink).toHaveAttribute('href', '/w/test-workspace/context');
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/unit/pages/agent-logs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ vi.mock("lucide-react", () => ({
}));

// --- Import page after all mocks are set up ---
import AgentLogsPage from "@/app/w/[slug]/agent-logs/page";
import AgentLogsPage from "@/app/w/[slug]/context/agent-logs/page";

// Helper: reset fetch to return an empty log list
function mockEmptyFetch() {
Expand Down
Loading
Loading