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
7 changes: 5 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
branches: [master, dev/delicious233, dev/trump]

env:
GO_VERSION: "1.25"
GO_VERSION: "1.25.11"
GOLANGCI_LINT_VERSION: "v2.12.2"
NODE_VERSION: "22"
PNPM_VERSION: "10"
Expand Down Expand Up @@ -329,11 +329,14 @@ jobs:
- name: Install
run: pnpm install --frozen-lockfile
working-directory: ./app
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
working-directory: ./app
- name: Build desktop
run: pnpm build
working-directory: ./app/desktop
- name: Smoke test
run: pnpm exec playwright test --project=chromium
run: pnpm exec playwright test --config e2e/playwright.config.ts --project=chromium
working-directory: ./app

# ── Validation ───────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion app/desktop/src/__tests__/hubClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
expires_in: 900,
};

function jsonResponse(status: number, data: unknown, statusText = status === 200 ? 'OK' : 'Error') {

Check warning on line 27 in app/desktop/src/__tests__/hubClient.test.ts

View workflow job for this annotation

GitHub Actions / frontend-desktop

'jsonResponse' is defined but never used
return new Response(JSON.stringify(data), {
status,
statusText,
Expand Down Expand Up @@ -522,7 +522,7 @@

await client.me();

expect(fetchSpy.mock.calls[0]?.[0]).toBe('https://api.hub.vectorcontrol.tech/client/auth/me');
expect(fetchSpy.mock.calls[0]?.[0]).toBe('http://localhost:8080/client/auth/me');
});
});
});
Expand Down
6 changes: 3 additions & 3 deletions app/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [['html'], ['json', { outputFile: 'results.json' }]] : 'html',
use: {
baseURL: 'http://127.0.0.1:5173',
baseURL: 'http://127.0.0.1:5175',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
Expand All @@ -22,8 +22,8 @@ export default defineConfig({
},
],
webServer: {
command: 'pnpm --filter agenthub-web dev',
port: 5173,
command: 'corepack pnpm --filter agenthub-web dev --host 127.0.0.1',
port: 5175,
reuseExistingServer: !process.env.CI,
},
});
2 changes: 1 addition & 1 deletion app/shared/src/ui/ArtifactPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Download,
ArrowRight,
} from 'lucide-react';
import Modal from '@shared/ui/Modal';
import Modal from './Modal';
import styles from './ArtifactPreview.module.css';

export type ArtifactType = 'iframe' | 'page' | 'image' | 'file';
Expand Down
3 changes: 1 addition & 2 deletions app/web/src/__e2e__/oidc-login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ interface MockOIDCParams {
function mockOIDCFlow(page: import('@playwright/test').Page, params: MockOIDCParams = {}) {
const {
state = 'web-test-state-mock-12345',
code = 'web-test-auth-code-67890',
authError,
tokenError,
deviceId = '00000000-0000-0000-0000-000000000002',
Expand Down Expand Up @@ -139,7 +138,7 @@ test.describe('Web OIDC Login — Happy Path', () => {
});

test('callback URL completes full OIDC login cycle', async ({ page }) => {
const counters = mockOIDCFlow(page);
mockOIDCFlow(page);

// Plant pending PKCE data in sessionStorage
await page.goto('/');
Expand Down
4 changes: 2 additions & 2 deletions app/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Copy, RefreshCw, Trash2, ArrowDown, FileText, Pencil, Terminal, Search, FolderOpen, Globe, Bot, CheckSquare, Wrench, ChevronDown, AlertTriangle, ExternalLink, RefreshCcw, Reply, X } from 'lucide-react';
import { Copy, RefreshCw, Trash2, ArrowDown, FileText, Pencil, Terminal, Search, FolderOpen, Globe, Bot, CheckSquare, Wrench, AlertTriangle, ExternalLink, RefreshCcw, Reply } from 'lucide-react';
import type { ChatMessage, MessageBlock, ToolResultBlock, FileDiff, ReplyTarget } from './ChatView.types';
import MarkdownRenderer from './MarkdownRenderer';
import CodeBlock from './CodeBlock';
Expand Down Expand Up @@ -589,7 +589,7 @@ function extractMessageText(msg: ChatMessage): string {
}

// ── ChatView ────────────────────────────────
export default function ChatView({ messages, isStreaming, onRetry, onDelete, onReply, onRegenerate, replyTo, onCancelReply }: Props) {
export default function ChatView({ messages, isStreaming, onRetry, onDelete, onReply, onRegenerate, replyTo }: Props) {
const { t, i18n } = useTranslation();
const addToast = useToastStore((s) => s.addToast);
const scrollRef = useRef<HTMLDivElement>(null);
Expand Down
2 changes: 1 addition & 1 deletion app/web/src/components/IM/IMContactList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useMemo, useCallback, memo } from 'react';
import { MessageSquare, Plus, SearchX, Pin } from 'lucide-react';
import { MessageSquare, Plus, Pin } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { EmptyState } from '@shared/ui';
import type { IMContact } from './types';
Expand Down
2 changes: 1 addition & 1 deletion app/web/src/components/IM/IMMessageView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useEffect, memo, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next';
import { MessageSquareText, CornerUpLeft, Forward, RotateCcw, X } from 'lucide-react';
import { MessageSquareText, CornerUpLeft, Forward, RotateCcw } from 'lucide-react';
import { EmptyState, MessageBubble } from '@shared/ui';
import MarkdownRenderer from '@/components/MarkdownRenderer';
import type { IMMessage } from './types';
Expand Down
2 changes: 0 additions & 2 deletions app/web/src/components/IM/TeamApprovalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ interface TeamApprovalPanelProps {
memberNames: Record<string, string>;
}

const DECIDED_STATUSES = new Set(['approved', 'denied', 'allow', 'deny', 'decided', 'resolved']);

function isPending(a: TeamApprovalState): boolean {
const s = a.status.toLowerCase();
return ['pending', 'requested', 'waiting', 'waiting_for_approval'].includes(s);
Expand Down
4 changes: 0 additions & 4 deletions app/web/src/components/RunDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,11 @@ export default function RunDetail({
const hubClient = useMemo(() => createHubClient({ getToken: getAccessToken }), []);

const [summary, setSummary] = useState<AgentRunEventSummary | null>(null);
const [summaryError, setSummaryError] = useState(false);

useEffect(() => {
const taskId = run?.runId;
if (!taskId || !getAccessToken()) {
setSummary(null);
setSummaryError(false);
return;
}

Expand All @@ -175,13 +173,11 @@ export default function RunDetail({
.then((data) => {
if (!cancelled) {
setSummary(data);
setSummaryError(false);
}
})
.catch(() => {
if (!cancelled) {
setSummary(null);
setSummaryError(true);
}
});

Expand Down
2 changes: 0 additions & 2 deletions app/web/src/hooks/useStreamRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useTranslation } from 'react-i18next';
import { useEffect, useRef, useCallback } from 'react';
import { useConnectionStore } from '@/stores/connectionStore';
import { createHubClient, type AgentRunEvent } from '@/api/hubClient';
import { getAccessToken } from '@/hooks/useAuth';
import { mergeAgentRunEvents } from '@/utils/hubAdapters';

/**
Expand Down Expand Up @@ -34,7 +33,6 @@ export function useStreamRecovery({
isConnected: boolean;
}) {
const { t } = useTranslation();
const recoveryState = useConnectionStore((s) => s.recoveryState);
const setRecoveryState = useConnectionStore((s) => s.setRecoveryState);
const setRecoveryError = useConnectionStore((s) => s.setRecoveryError);
const setLastEventSeq = useConnectionStore((s) => s.setLastEventSeq);
Expand Down
4 changes: 2 additions & 2 deletions app/web/src/pages/AgentSquare.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { useCallback, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { Bot, Search, Sparkles, Star } from 'lucide-react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/contexts/ThemeContext';
Expand All @@ -14,7 +14,7 @@ function AgentSquarePage() {
const { t } = useTranslation('agentSquare');
const { hasSession, token } = useHubSession();

const { data: agentData, isLoading: agentLoading } = useAgentList(true);
const { data: agentData } = useAgentList(true);
const agents: AgentInfo[] = agentData?.items ?? [];

const hubCustomAgents = useHubCustomAgents(token);
Expand Down
31 changes: 22 additions & 9 deletions app/web/src/pages/PrivateChats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ThemeProvider } from '@/contexts/ThemeContext';
import { queryClient } from '@/api/queryClient';
import { useHubIMSnapshot } from '@/hooks/useHubIMSnapshot';
import { useHubSession } from '@/hooks/useHubSession';
import { renderHubContent } from '@/utils/hubAdapters';
import { EmptyState, SectionHeader, ActivityCard } from '@shared/ui';
import { useState, useMemo } from 'react';
import type { TFunction } from 'i18next';
Expand Down Expand Up @@ -34,14 +35,19 @@ function PrivateChatsPage() {
if (!hasSession || snapshot.status !== 'ready') return [];
return snapshot.sessions
.filter((s) => s.type === 'private')
.map((s) => ({
id: s.session_id ?? s.id ?? '',
name: s.name ?? t('hub.privateSessionFallback'),
lastMessage: s.last_message?.content ?? t('hub.noRecentMessages'),
time: formatRelativeTime(s.last_message_at, t),
unread: s.unread_count ?? 0,
online: true,
}));
.map((s) => {
const sessionId = s.session_id ?? s.id ?? '';
const messages = snapshot.messagesBySessionId[sessionId] ?? [];
const latestMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
return {
id: sessionId,
name: s.name ?? t('hub.privateSessionFallback'),
lastMessage: renderHubContent(latestMessage?.content ?? s.last_message?.content) || t('hub.noRecentMessages'),
time: formatRelativeTime(latestMessage?.created_at ?? s.last_message_at, t),
unread: s.unread_count ?? 0,
online: true,
};
});
}, [hasSession, snapshot, t]);

const filteredChats = searchQuery
Expand Down Expand Up @@ -88,7 +94,14 @@ function PrivateChatsPage() {
/>
</div>

{filteredChats.length === 0 ? (
{snapshot.status === 'error' ? (
<EmptyState
title={t('error.title')}
description={t('error.description', { error: snapshot.error ?? t('status.error') })}
icon={<MessageSquare size={24} />}
titleLevel={3}
/>
) : filteredChats.length === 0 ? (
<EmptyState
title={t('sidebar.emptyTitle')}
description={t('sidebar.empty')}
Expand Down
2 changes: 1 addition & 1 deletion app/web/src/pages/Project.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { FolderKanban, CheckSquare, FileText, Play, AlertTriangle, Search, User } from 'lucide-react';
import { FolderKanban, CheckSquare, FileText, Play, Search, User } from 'lucide-react';
import { QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from '@/contexts/ThemeContext';
import { queryClient } from '@/api/queryClient';
Expand Down
22 changes: 11 additions & 11 deletions app/web/src/pages/mockConvergence.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,29 +57,29 @@ describe('Web mock convergence states', () => {
vi.restoreAllMocks();
});

it('labels Project fallback data as demo/mock when Edge has no snapshot', () => {
it('locks Project when there is no Web Hub session', () => {
render(<ProjectPageInteractive />);

expect(screen.getAllByText('Demo / mock fallback').length).toBeGreaterThan(0);
expect(screen.getByText('Demo active tasks')).toBeInTheDocument();
expect(screen.getByText('Demo shared files')).toBeInTheDocument();
expect(screen.queryByText('source.mockFallback')).not.toBeInTheDocument();
expect(screen.getAllByText('Hub session required').length).toBeGreaterThan(0);
expect(screen.getByText('Please sign in to Hub to view project data and milestones.')).toBeInTheDocument();
expect(screen.queryByText('Demo / mock fallback')).not.toBeInTheDocument();
expect(screen.queryByText('Demo active tasks')).not.toBeInTheDocument();
});

it('labels Group Workspace fallback counts as demo when Edge has no snapshot', () => {
it('locks Group Workspace when there is no Web Hub session', () => {
render(<GroupWorkspacePageInteractive />);

expect(screen.getAllByText('Demo / mock fallback').length).toBeGreaterThan(0);
expect(screen.getByText('Demo online members')).toBeInTheDocument();
expect(screen.getByText('Demo shared tasks')).toBeInTheDocument();
expect(screen.getByText('Demo workspace files')).toBeInTheDocument();
expect(screen.getAllByText('Local workspace ready').length).toBeGreaterThan(0);
expect(screen.getByText('Please sign in to Hub to view group workspace data.')).toBeInTheDocument();
expect(screen.queryByText('Demo / mock fallback')).not.toBeInTheDocument();
expect(screen.queryByText('Demo online members')).not.toBeInTheDocument();
});

it('locks Private Chats when there is no Web Hub session', () => {
render(<PrivateChatsPageInteractive />);

expect(screen.getAllByText('Hub session required').length).toBeGreaterThan(0);
expect(screen.getByText(/will not show mock conversations/i)).toBeInTheDocument();
expect(screen.getAllByText(/will not show mock conversations/i).length).toBeGreaterThan(0);
expect(screen.queryByText('Local mock')).not.toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalled();
});
Expand Down
3 changes: 1 addition & 2 deletions app/web/src/stores/connectionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ export const useConnectionStore = create<ConnectionState>()(
})),
clearLastEventSeq: (taskId) =>
set((s) => {
const next = { ...s.lastEventSeq };
delete next[taskId];
const { [taskId]: _removed, ...next } = s.lastEventSeq;
return { lastEventSeq: next };
}),
setRecoveryState: (v) => set({ recoveryState: v }),
Expand Down
1 change: 0 additions & 1 deletion app/web/src/views/IMView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next';
import {
Forward,
LogIn,
MessageCircle,
MessageSquare,
ShieldCheck,
TerminalSquare,
Expand Down
5 changes: 2 additions & 3 deletions app/web/src/views/TeamRunConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useHubAgentTeams, useCreateAgentTeam, useStartTeamRun, useDecideTeamApp
import { createHubClient } from '@/api/hubClient';
import type {
AgentTeamDetail,
AgentTeamEvent,
AgentTeamRun,
AgentTeamTask,
TeamRunState,
Expand All @@ -31,8 +32,6 @@ import styles from './TeamRunConsole.module.css';

// ── helpers ──

type TeamRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled';

function statusLabelKey(status: string): string {
return `teamRun.status.${status}`;
}
Expand Down Expand Up @@ -110,7 +109,7 @@ export default function TeamRunConsole(_props: ViewProps) {
const [activeTab, setActiveTab] = useState<ConsoleTab>('members');
const [localState, setLocalState] = useState<TeamRunState | null>(null);
const [localTasks, setLocalTasks] = useState<AgentTeamTask[]>([]);
const [localEvents, setLocalEvents] = useState<any[]>([]);
const [localEvents, setLocalEvents] = useState<AgentTeamEvent[]>([]);
const [stateLoading, setStateLoading] = useState(false);
const [decidingIds, setDecidingIds] = useState<Set<string>>(new Set());
const [insetTeam, setInsetTeam] = useState<AgentTeamDetail | null>(null);
Expand Down
Loading
Loading