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
9 changes: 5 additions & 4 deletions packages/docs-web/src/content/docs/adapters/web.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Accessible via the `/dashboard` route, the Command Center shows all workflow run

### Settings

The `/settings` page lets you configure assistant defaults (model, provider) without editing YAML files.
The `/settings` page lets you configure assistant defaults (model, provider) without editing YAML files. It also includes a **Projects** section for registering and managing codebases.

## Chat Interface

Expand Down Expand Up @@ -203,10 +203,11 @@ A separate dashboard SSE stream at `/api/stream/__dashboard__` multiplexes workf

### Registering a Project

From the Web UI, you can register codebases in two ways:
From the Web UI, you can register codebases in three ways:

1. **Clone from URL** -- Use the `/clone <url>` command in chat, or use the API to POST to `/api/codebases` with a `url` field
2. **Register a local path** -- POST to `/api/codebases` with a `path` field pointing to an existing git repository
1. **Add Project input** -- Click **+** in the sidebar or go to **Settings → Projects** and enter a GitHub URL or local path. Inputs starting with `https://`, `ssh://`, `git@`, or `git://` are treated as remote URLs (cloned); everything else is treated as a local path (registered in place).
2. **Clone from URL via chat** -- Use the `/clone <url>` command in chat, or use the API to POST to `/api/codebases` with a `url` field
3. **Register a local path via API** -- POST to `/api/codebases` with a `path` field pointing to an existing git repository

Registered codebases appear in the sidebar's project selector.

Expand Down
9 changes: 2 additions & 7 deletions packages/web/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ProjectDetail } from '@/components/sidebar/ProjectDetail';
import { AllConversationsView } from '@/components/sidebar/AllConversationsView';
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
import { useProject } from '@/contexts/ProjectContext';
import { addCodebase } from '@/lib/api';
import { addCodebase, getCodebaseInput } from '@/lib/api';

const SIDEBAR_MIN = 240;
const SIDEBAR_MAX = 400;
Expand Down Expand Up @@ -120,12 +120,7 @@ export function Sidebar(): React.ReactElement {
setAddLoading(true);
setAddError(null);

// Detect: starts with / or ~ or Windows drive letter → local path; otherwise → URL
const isLocalPath =
trimmed.startsWith('/') || trimmed.startsWith('~') || /^[A-Za-z]:[/\\]/.test(trimmed);
const input = isLocalPath ? { path: trimmed } : { url: trimmed };

void addCodebase(input)
void addCodebase(getCodebaseInput(trimmed))
.then(codebase => {
void queryClient.invalidateQueries({ queryKey: ['codebases'] });
handleSelectProject(codebase.id);
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const SSE_BASE_URL = import.meta.env.DEV
? `http://${window.location.hostname}:${apiPort}`
: '';

export { getCodebaseInput } from '@/lib/codebase-input';

export interface ConversationResponse {
id: string;
platform_type: string;
Expand Down
64 changes: 64 additions & 0 deletions packages/web/src/lib/codebase-input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, test } from 'bun:test';
import { getCodebaseInput } from '@/lib/codebase-input';

describe('getCodebaseInput', () => {
test('treats GitHub repository inputs as urls', () => {
expect(getCodebaseInput('https://github.com/coleam00/Archon')).toEqual({
url: 'https://github.com/coleam00/Archon',
});
});

test('treats SSH git@ shorthand as urls', () => {
expect(getCodebaseInput('git@github.com:coleam00/Archon.git')).toEqual({
url: 'git@github.com:coleam00/Archon.git',
});
});

test('treats ssh:// URLs as urls', () => {
expect(getCodebaseInput('ssh://git@github.com/coleam00/Archon.git')).toEqual({
url: 'ssh://git@github.com/coleam00/Archon.git',
});
});

test('treats git:// URLs as urls', () => {
expect(getCodebaseInput('git://github.com/coleam00/Archon.git')).toEqual({
url: 'git://github.com/coleam00/Archon.git',
});
});

test('trims surrounding whitespace before classifying', () => {
expect(getCodebaseInput(' https://github.com/a/b ')).toEqual({
url: 'https://github.com/a/b',
});
});

test('treats relative local paths as paths', () => {
expect(getCodebaseInput('./repo')).toEqual({ path: './repo' });
expect(getCodebaseInput('../repo')).toEqual({ path: '../repo' });
expect(getCodebaseInput('repo')).toEqual({ path: 'repo' });
});

test('treats unix local paths as paths', () => {
expect(getCodebaseInput('/path/to/repository')).toEqual({
path: '/path/to/repository',
});
});

test('treats home-relative paths as paths', () => {
expect(getCodebaseInput('~/src/archon')).toEqual({
path: '~/src/archon',
});
});

test('treats windows local paths as paths', () => {
expect(getCodebaseInput('C:\\repo\\archon')).toEqual({
path: 'C:\\repo\\archon',
});
});

test('treats windows UNC paths as paths', () => {
expect(getCodebaseInput('\\\\server\\share\\archon')).toEqual({
path: '\\\\server\\share\\archon',
});
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
10 changes: 10 additions & 0 deletions packages/web/src/lib/codebase-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Classify input for POST /api/codebases. A `url` key signals a remote clone;
* a `path` key signals registering a local/relative path (server resolves
* tilde/relative). Inputs without an explicit remote prefix fall through to `path`.
*/
export function getCodebaseInput(value: string): { path: string } | { url: string } {
const trimmed = value.trim();
const isRemoteUrl = /^(https?:\/\/|ssh:\/\/|git@|git:\/\/)/i.test(trimmed);
return isRemoteUrl ? { url: trimmed } : { path: trimmed };
}
8 changes: 2 additions & 6 deletions packages/web/src/routes/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ConversationItem } from '@/components/conversations/ConversationItem';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
import { useProject } from '@/contexts/ProjectContext';
import { listConversations, listWorkflowRuns, addCodebase } from '@/lib/api';
import { listConversations, listWorkflowRuns, addCodebase, getCodebaseInput } from '@/lib/api';
import type { CodebaseResponse } from '@/lib/api';
import { cn } from '@/lib/utils';

Expand Down Expand Up @@ -146,11 +146,7 @@ export function ChatPage(): React.ReactElement {
setAddLoading(true);
setAddError(null);

const isLocalPath =
trimmed.startsWith('/') || trimmed.startsWith('~') || /^[A-Za-z]:[/\\]/.test(trimmed);
const input = isLocalPath ? { path: trimmed } : { url: trimmed };

void addCodebase(input)
void addCodebase(getCodebaseInput(trimmed))
.then(codebase => {
void queryClient.invalidateQueries({ queryKey: ['codebases'] });
setSelectedProjectId(codebase.id);
Expand Down
19 changes: 10 additions & 9 deletions packages/web/src/routes/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
listCodebases,
listProviders,
addCodebase,
getCodebaseInput,
deleteCodebase,
updateAssistantConfig,
getCodebaseEnvVars,
Expand Down Expand Up @@ -258,7 +259,7 @@ function EnvVarsPanel({ codebaseId }: { codebaseId: string }): React.ReactElemen

function ProjectsSection(): React.ReactElement {
const queryClient = useQueryClient();
const [addPath, setAddPath] = useState('');
const [addValue, setAddValue] = useState('');
const [showAdd, setShowAdd] = useState(false);
const [expandedEnvVars, setExpandedEnvVars] = useState<string | null>(null);

Expand All @@ -268,10 +269,10 @@ function ProjectsSection(): React.ReactElement {
});

const addMutation = useMutation({
mutationFn: ({ path }: { path: string }) => addCodebase({ path }),
mutationFn: (value: string) => addCodebase(getCodebaseInput(value)),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['codebases'] });
setAddPath('');
setAddValue('');
setShowAdd(false);
},
});
Expand All @@ -285,8 +286,8 @@ function ProjectsSection(): React.ReactElement {

function handleAddSubmit(e: React.FormEvent): void {
e.preventDefault();
if (addPath.trim()) {
addMutation.mutate({ path: addPath.trim() });
if (addValue.trim()) {
addMutation.mutate(addValue.trim());
}
}

Expand Down Expand Up @@ -339,11 +340,11 @@ function ProjectsSection(): React.ReactElement {
{showAdd ? (
<form onSubmit={handleAddSubmit} className="mt-3 flex gap-2">
<Input
value={addPath}
value={addValue}
onChange={e => {
setAddPath(e.target.value);
setAddValue(e.target.value);
}}
placeholder="/path/to/repository"
placeholder="GitHub URL or local path"
className="flex-1"
/>
<Button type="submit" size="sm" disabled={addMutation.isPending}>
Expand All @@ -355,7 +356,7 @@ function ProjectsSection(): React.ReactElement {
size="sm"
onClick={() => {
setShowAdd(false);
setAddPath('');
setAddValue('');
}}
>
Cancel
Expand Down
Loading