Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f06a66a
feat(web): add MobileNavContext for mobile sidebar state
tboome33 Apr 13, 2026
edce118
feat(web): responsive mobile Layout with drawer sidebar
tboome33 Apr 13, 2026
27c8b53
feat(web): add hamburger menu button in TopNav for mobile
tboome33 Apr 13, 2026
d7577f6
feat(web): hide conversation panel on mobile for full-width chat
tboome33 Apr 13, 2026
a45ec74
fix(api): fix SSE URL construction for Cloudflare tunnel access
tboome33 Apr 13, 2026
c49aaa9
feat(mobile): add conversation drawer and overflow-x fix for mobile
tboome33 Apr 13, 2026
a19e630
fix(mobile): prevent horizontal scroll with overflow-x hidden on html…
tboome33 Apr 13, 2026
840822f
fix(mobile): sticky chat input + h-dvh layout + sticky mobile topbar
tboome33 Apr 13, 2026
720c1ed
fix(chat): flex-shrink-0 + safe-area-inset-bottom on MessageInput
tboome33 Apr 13, 2026
ba0dbc7
fix(mobile): sticky mobile topbar (hamburger + New button always visi…
tboome33 Apr 13, 2026
f74f344
fix(mobile): drawer starts below sticky topbar on mobile
tboome33 Apr 13, 2026
124d8cc
fix(mobile): drawer nav starts below topbar (top-12) and z-40
tboome33 Apr 13, 2026
329924a
fix(mobile): top-12 -> top-24 on nav drawer and backdrop (Layout)
tboome33 Apr 13, 2026
fcb18f5
fix(mobile): top-12 -> top-24 on conv drawer and backdrop (ChatPage)
tboome33 Apr 13, 2026
0b2cbb1
fix(mobile): drawer top-12, z-50, opaque bg - covers second topbar
tboome33 Apr 13, 2026
a7236ca
fix(mobile): drawer top-12, z-50, opaque bg - second topbar z-30
tboome33 Apr 13, 2026
239d111
fix(mobile): clavier virtuel masque l'input + scrollbars parasites
tboome33 Apr 13, 2026
2bb9d41
fix(layout): add position:fixed to root container for iOS keyboard fix
tboome33 Apr 13, 2026
7eae91e
fix(web): fix 404 on conversation delete, translate FR labels to EN, …
tboome33 Apr 13, 2026
3c9c768
docs: add Cloudflare Quick Tunnel setup guide
tboome33 Apr 13, 2026
5325681
feat(web): add Appearance settings section with theme and responsive …
tboome33 Apr 13, 2026
0c398b5
feat(web): add Cloudflare tunnel manager in TopNav with start/stop/co…
tboome33 Apr 13, 2026
4c61f54
fix(web): make workflow builder responsive on mobile screens
tboome33 Apr 13, 2026
f2b28ea
chore(lint): exclude pre-existing format files from lint and prettier
tboome33 Apr 13, 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
120 changes: 107 additions & 13 deletions packages/web/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,107 @@
import { Outlet } from 'react-router';
import { TopNav } from './TopNav';

export function Layout(): React.ReactElement {
return (
<div className="flex h-screen flex-col bg-background">
<TopNav />
<main className="flex flex-1 flex-col overflow-hidden">
<Outlet />
</main>
</div>
);
}
import { useState } from 'react';
import { Outlet, NavLink } from 'react-router';
import { MessageSquare, LayoutDashboard, Workflow, Settings, X } from 'lucide-react';
import { TopNav } from './TopNav';
import { MobileNavContext } from '@/contexts/MobileNavContext';
import { cn } from '@/lib/utils';

const navItems = [
{ to: '/chat', end: false, icon: MessageSquare, label: 'Chat' },
{ to: '/dashboard', end: true, icon: LayoutDashboard, label: 'Dashboard' },
{ to: '/workflows', end: false, icon: Workflow, label: 'Workflows' },
] as const;

export function Layout(): React.ReactElement {
const [open, setOpen] = useState(false);

return (
<MobileNavContext.Provider value={{ open, setOpen }}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset the mobile nav state on desktop resize.

Line 15 keeps open alive, and Lines 25-42 only hide the drawer with responsive classes. If the menu is open during a rotate/resize, switching back to mobile reopens it unexpectedly.

🔧 Proposed fix
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
@@
 export function Layout(): React.ReactElement {
   const [open, setOpen] = useState(false);
+
+  useEffect((): (() => void) => {
+    const onResize = (): void => {
+      if (window.innerWidth >= 768) {
+        setOpen(false);
+      }
+    };
+
+    window.addEventListener('resize', onResize);
+    return (): void => {
+      window.removeEventListener('resize', onResize);
+    };
+  }, []);
 
   return (

Also applies to: 25-42

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/components/layout/Layout.tsx` around lines 15 - 18, The
mobile nav state (open from useState in Layout and provided via
MobileNavContext.Provider) isn't reset when switching to desktop, causing the
drawer to reopen when returning to mobile; add a useEffect inside the Layout
component that listens for viewport changes (e.g., window.matchMedia for the
desktop breakpoint or window resize) and calls setOpen(false) when the viewport
crosses into desktop size, and ensure the listener is cleaned up on unmount.

<div className="flex h-screen flex-col bg-background">
<TopNav />

{/* ── Mobile nav overlay backdrop ── */}
{open && (
<div
className="fixed inset-0 z-40 bg-black/60 md:hidden"
onClick={() => { setOpen(false); }}
aria-hidden="true"
/>
)}

{/* ── Mobile nav drawer ── */}
<aside
className={cn(
'fixed inset-y-0 left-0 z-50 flex w-72 flex-col bg-surface border-r border-border shadow-2xl',
'transition-transform duration-300 ease-in-out',
'md:hidden',
open ? 'translate-x-0' : '-translate-x-full'
)}
aria-label="Navigation mobile"
>
{/* Drawer header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2">
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary">
<span className="text-sm font-semibold text-primary-foreground">A</span>
</div>
<span className="text-sm font-semibold text-text-primary">Archon</span>
</div>
<button
onClick={() => { setOpen(false); }}
className="flex items-center justify-center rounded-md p-1.5 text-text-secondary hover:bg-surface-elevated hover:text-text-primary transition-colors"
aria-label="Fermer le menu"
>
<X className="h-4 w-4" />
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>

{/* Navigation links */}
<nav className="flex-1 overflow-y-auto p-2 pt-3">
{navItems.map(({ to, end, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={end}
onClick={() => { setOpen(false); }}
className={({ isActive }: { isActive: boolean }): string =>
cn(
'flex items-center gap-3 w-full rounded-md px-3 py-2.5 text-sm font-medium transition-colors mb-0.5',
isActive
? 'bg-accent text-primary'
: 'text-text-secondary hover:bg-surface-elevated hover:text-text-primary'
)
}
>
<Icon className="h-4 w-4 shrink-0" />
{label}
</NavLink>
))}
</nav>

{/* Settings — pinned at bottom */}
<div className="p-2 border-t border-border">
<NavLink
to="/settings"
onClick={() => { setOpen(false); }}
className={({ isActive }: { isActive: boolean }): string =>
cn(
'flex items-center gap-3 w-full rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
isActive
? 'bg-accent text-primary'
: 'text-text-secondary hover:bg-surface-elevated hover:text-text-primary'
)
}
>
<Settings className="h-4 w-4 shrink-0" />
Settings
</NavLink>
</div>
</aside>

<main className="flex flex-1 flex-col overflow-hidden">
<Outlet />
</main>
</div>
</MobileNavContext.Provider>
);
}
175 changes: 97 additions & 78 deletions packages/web/src/components/layout/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,97 @@
import { NavLink, Link } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { LayoutDashboard, MessageSquare, Workflow, Settings } from 'lucide-react';
import { listWorkflowRuns, getUpdateCheck } from '@/lib/api';
import { cn } from '@/lib/utils';

const tabs = [
{ to: '/chat', end: false, icon: MessageSquare, label: 'Chat' },
{ to: '/dashboard', end: true, icon: LayoutDashboard, label: 'Dashboard' },
{ to: '/workflows', end: false, icon: Workflow, label: 'Workflows' },
{ to: '/settings', end: false, icon: Settings, label: 'Settings' },
] as const;

export function TopNav(): React.ReactElement {
const { data: runningRuns } = useQuery({
queryKey: ['workflowRuns', { status: 'running' }],
queryFn: () => listWorkflowRuns({ status: 'running', limit: 1 }),
refetchInterval: 10_000,
});
const hasRunning = (runningRuns?.length ?? 0) > 0;

const { data: updateCheck } = useQuery({
queryKey: ['update-check'],
queryFn: getUpdateCheck,
staleTime: 60 * 60 * 1000,
refetchInterval: 60 * 60 * 1000,
retry: false,
});

return (
<nav className="flex items-center gap-1 border-b border-border bg-surface px-4">
{/* Brand logo */}
<Link to="/chat" className="flex items-center gap-2 mr-4 hover:opacity-80 transition-opacity">
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary">
<span className="text-sm font-semibold text-primary-foreground">A</span>
</div>
<span className="text-sm font-semibold text-text-primary">Archon</span>
</Link>

{tabs.map(({ to, end, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }: { isActive: boolean }): string =>
cn(
'flex items-center gap-2 px-3 py-3 text-sm font-medium border-b-2 transition-colors',
isActive
? 'border-primary text-primary'
: 'border-transparent text-text-secondary hover:text-text-primary'
)
}
>
<Icon className="h-4 w-4" />
{label}
{to === '/dashboard' && hasRunning && (
<span className="flex h-2 w-2 rounded-full bg-primary animate-pulse" />
)}
</NavLink>
))}
<span className="ml-auto text-xs text-text-secondary">
v{import.meta.env.VITE_APP_VERSION as string}
{updateCheck?.updateAvailable && updateCheck.releaseUrl && (
<a
href={updateCheck.releaseUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-1.5 inline-flex items-center gap-1 text-xs text-primary hover:underline"
title={`v${updateCheck.latestVersion} available`}
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />v
{updateCheck.latestVersion}
</a>
)}
</span>
</nav>
);
}
import { NavLink, Link } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { LayoutDashboard, MessageSquare, Workflow, Settings, Menu } from 'lucide-react';
import { listWorkflowRuns, getUpdateCheck } from '@/lib/api';
import { cn } from '@/lib/utils';
import { useMobileNav } from '@/contexts/MobileNavContext';

const tabs = [
{ to: '/chat', end: false, icon: MessageSquare, label: 'Chat' },
{ to: '/dashboard', end: true, icon: LayoutDashboard, label: 'Dashboard' },
{ to: '/workflows', end: false, icon: Workflow, label: 'Workflows' },
{ to: '/settings', end: false, icon: Settings, label: 'Settings' },
] as const;

export function TopNav(): React.ReactElement {
const { setOpen } = useMobileNav();

const { data: runningRuns } = useQuery({
queryKey: ['workflowRuns', { status: 'running' }],
queryFn: () => listWorkflowRuns({ status: 'running', limit: 1 }),
refetchInterval: 10_000,
});
const hasRunning = (runningRuns?.length ?? 0) > 0;

const { data: updateCheck } = useQuery({
queryKey: ['update-check'],
queryFn: getUpdateCheck,
staleTime: 60 * 60 * 1000,
refetchInterval: 60 * 60 * 1000,
retry: false,
});

return (
<nav className="flex items-center gap-1 border-b border-border bg-surface px-4">

{/* ── Mobile: hamburger button (hidden on desktop) ── */}
<button
onClick={() => { setOpen(true); }}
className="flex items-center justify-center rounded-md p-1.5 mr-2 text-text-secondary hover:bg-surface-elevated hover:text-text-primary transition-colors md:hidden"
aria-label="Ouvrir le menu"
>
<Menu className="h-5 w-5" />
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

{/* Brand logo */}
<Link to="/chat" className="flex items-center gap-2 mr-4 hover:opacity-80 transition-opacity">
<div className="flex h-7 w-7 items-center justify-center rounded-md bg-primary">
<span className="text-sm font-semibold text-primary-foreground">A</span>
</div>
{/* Logo text: always visible */}
<span className="text-sm font-semibold text-text-primary">Archon</span>
</Link>

{/* ── Desktop nav tabs (hidden on mobile) ── */}
<div className="hidden md:flex items-center gap-1">
{tabs.map(({ to, end, icon: Icon, label }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }: { isActive: boolean }): string =>
cn(
'flex items-center gap-2 px-3 py-3 text-sm font-medium border-b-2 transition-colors',
isActive
? 'border-primary text-primary'
: 'border-transparent text-text-secondary hover:text-text-primary'
)
}
>
<Icon className="h-4 w-4" />
{label}
{to === '/dashboard' && hasRunning && (
<span className="flex h-2 w-2 rounded-full bg-primary animate-pulse" />
)}
</NavLink>
))}
</div>

{/* Version + update badge */}
<span className="ml-auto text-xs text-text-secondary">
v{import.meta.env.VITE_APP_VERSION as string}
{updateCheck?.updateAvailable && updateCheck.releaseUrl && (
<a
href={updateCheck.releaseUrl}
target="_blank"
rel="noopener noreferrer"
className="ml-1.5 inline-flex items-center gap-1 text-xs text-primary hover:underline"
title={`v${updateCheck.latestVersion} available`}
>
<span className="inline-block h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />v
{updateCheck.latestVersion}
</a>
)}
</span>
</nav>
);
}
15 changes: 15 additions & 0 deletions packages/web/src/contexts/MobileNavContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createContext, useContext } from 'react';

export interface MobileNavContextValue {
open: boolean;
setOpen: (open: boolean) => void;
}

export const MobileNavContext = createContext<MobileNavContextValue>({
open: false,
setOpen: () => {},
});

export function useMobileNav(): MobileNavContextValue {
return useContext(MobileNavContext);
}
27 changes: 21 additions & 6 deletions packages/web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ export type WorkflowDefinition = components['schemas']['WorkflowDefinition'];
export type DagNode = components['schemas']['DagNode'];

/**
* Base URL for SSE streams. In dev, bypasses Vite proxy by connecting directly
* to the backend server. In production, uses relative URLs (same origin).
* Uses the page hostname so it works from any network interface.
* Base URL for SSE streams.
* - On localhost (direct dev access): connect directly to backend, bypassing Vite proxy
* (avoids proxy buffering of SSE streams).
* - On any other hostname (Cloudflare tunnel, remote access): use relative URL so
* requests go through the Vite proxy, which forwards to the backend.
* This is required because the backend port is not directly accessible remotely.
*/
const apiPort = (import.meta.env.VITE_API_PORT as string | undefined) ?? '3090';
export const SSE_BASE_URL = import.meta.env.DEV
? `http://${window.location.hostname}:${apiPort}`
export const SSE_BASE_URL = import.meta.env.DEV && window.location.hostname === 'localhost'
? `http://localhost:${apiPort}`
: '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard module-load window access for SSR/tests.

Line 21 can throw when this module is evaluated without window (SSR/tests). Use a lazy helper with a typeof window guard.

Suggested fix
 const apiPort = (import.meta.env.VITE_API_PORT as string | undefined) ?? '3090';
-export const SSE_BASE_URL = import.meta.env.DEV && window.location.hostname === 'localhost'
-  ? `http://localhost:${apiPort}`
-  : '';
+function getSSEBaseUrl(): string {
+  const hostname = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
+  return import.meta.env.DEV && hostname === 'localhost' ? `http://localhost:${apiPort}` : '';
+}
+
+export const SSE_BASE_URL = getSSEBaseUrl();

Based on learnings: implement SSE URL via a lazy getSSEBaseUrl() with typeof window !== 'undefined' and 'localhost' fallback to avoid SSR/test crashes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/api.ts` around lines 21 - 23, Replace the top-level
SSE_BASE_URL constant with a lazy function getSSEBaseUrl() that guards access to
window using typeof window !== 'undefined'; inside the function check
import.meta.env.DEV and only then read window.location.hostname === 'localhost'
to return `http://localhost:${apiPort}` or '' as fallback, so module evaluation
during SSR/tests won't touch window; update any callers to invoke
getSSEBaseUrl() instead of referencing SSE_BASE_URL.


export interface ConversationResponse {
Expand All @@ -38,6 +41,7 @@ export interface CodebaseResponse {
repository_url: string | null;
default_cwd: string;
ai_assistant_type: string;
allow_env_keys: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify backend contract support for allowEnvKeys / allow_env_keys
set -euo pipefail

echo "== Server schemas =="
rg -n --type=ts -C3 'codebaseSchema|addCodebaseBodySchema|allowEnvKeys|allow_env_keys' packages/server/src/routes/schemas

echo "== Server API routes =="
rg -n --type=ts -C3 '/api/codebases|registerOpenApiRoute|allowEnvKeys|allow_env_keys' packages/server/src/routes/api.ts

echo "== Generated web API types =="
rg -n --type=ts -C2 'allow_env_keys|Codebase' packages/web/src/lib/api.generated.ts

Repository: coleam00/Archon

Length of output: 15737


🏁 Script executed:

# Check the web client file at the mentioned lines
cat -n packages/web/src/lib/api.ts | sed -n '40,50p;160,185p'

Repository: coleam00/Archon

Length of output: 1455


🏁 Script executed:

# Read the complete codebase schema file
cat -n packages/server/src/routes/schemas/codebase.schemas.ts | head -50

Repository: coleam00/Archon

Length of output: 2013


🏁 Script executed:

# Search for generated API type files in the web package
fd -t f "api" packages/web/src/lib/ | head -20

Repository: coleam00/Archon

Length of output: 126


🏁 Script executed:

# Search for allowEnvKeys or allow_env_keys in web package
rg -n 'allowEnvKeys|allow_env_keys' packages/web/src/ -t ts

Repository: coleam00/Archon

Length of output: 307


🏁 Script executed:

# Search for updateCodebase schema or PATCH handling
rg -n 'updateCodebaseBodySchema|updateCodebaseSchema|PATCH.*codebases' packages/server/src/routes/ -t ts

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for any mention of allowEnvKeys on server side
rg -n 'allowEnvKeys|allow_env_keys' packages/server/src/ -t ts

Repository: coleam00/Archon

Length of output: 41


Fix client-server contract misalignment for allowEnvKeys field.

The client sends and expects allowEnvKeys across three operations, but the server provides no support:

  • POST /api/codebases (line 164): Client sends optional allowEnvKeys but server's addCodebaseBodySchema only accepts url and path—field is silently ignored.
  • PATCH /api/codebases/:id (line 175): Client function sends allowEnvKeys, but server has no PATCH handler or schema defined.
  • Response contract (line 44): Client's CodebaseResponse expects mandatory allow_env_keys field, but server's codebaseSchema does not include it.

Either add full server support (schema fields, handlers, database column if needed) or remove allowEnvKeys from the client API until the backend is ready.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/lib/api.ts` at line 44, Client-server contract mismatch: the
client uses allowEnvKeys / allow_env_keys but the server schemas and handlers
(addCodebaseBodySchema, codebaseSchema, and no PATCH handler for
/api/codebases/:id) don’t support it. Fix by either removing allowEnvKeys from
the client API or adding full server support: update addCodebaseBodySchema to
accept allow_env_keys, extend codebaseSchema to include allow_env_keys in
responses, implement a PATCH handler for /api/codebases/:id that validates and
persists allow_env_keys to the DB (and add a DB column/migration if needed), and
ensure the client and server use the same snake_case/key names in
request/response contracts.

commands: Record<string, { path: string; description: string }>;
created_at: string;
updated_at: string;
Expand Down Expand Up @@ -157,7 +161,7 @@ export async function getCodebase(id: string): Promise<CodebaseResponse> {
}

export async function addCodebase(
input: { url: string } | { path: string }
input: { url: string; allowEnvKeys?: boolean } | { path: string; allowEnvKeys?: boolean }
): Promise<CodebaseResponse> {
return fetchJSON<CodebaseResponse>('/api/codebases', {
method: 'POST',
Expand All @@ -166,6 +170,17 @@ export async function addCodebase(
});
}

export async function updateCodebase(
id: string,
input: { allowEnvKeys: boolean }
): Promise<CodebaseResponse> {
return fetchJSON<CodebaseResponse>(`/api/codebases/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
}

export async function deleteCodebase(id: string): Promise<{ success: boolean }> {
return fetchJSON<{ success: boolean }>(`/api/codebases/${id}`, { method: 'DELETE' });
}
Expand Down
Loading