diff --git a/packages/docs-web/src/content/docs/adapters/web.md b/packages/docs-web/src/content/docs/adapters/web.md index 79cde027..7a3aeebb 100644 --- a/packages/docs-web/src/content/docs/adapters/web.md +++ b/packages/docs-web/src/content/docs/adapters/web.md @@ -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 @@ -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 ` 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 ` 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. diff --git a/packages/web/src/components/layout/Sidebar.tsx b/packages/web/src/components/layout/Sidebar.tsx index b4b0e135..27e19248 100644 --- a/packages/web/src/components/layout/Sidebar.tsx +++ b/packages/web/src/components/layout/Sidebar.tsx @@ -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; @@ -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); diff --git a/packages/web/src/lib/api.ts b/packages/web/src/lib/api.ts index 72bcdc03..9d8c7add 100644 --- a/packages/web/src/lib/api.ts +++ b/packages/web/src/lib/api.ts @@ -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; diff --git a/packages/web/src/lib/codebase-input.test.ts b/packages/web/src/lib/codebase-input.test.ts new file mode 100644 index 00000000..4909e57a --- /dev/null +++ b/packages/web/src/lib/codebase-input.test.ts @@ -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', + }); + }); +}); diff --git a/packages/web/src/lib/codebase-input.ts b/packages/web/src/lib/codebase-input.ts new file mode 100644 index 00000000..21052e1f --- /dev/null +++ b/packages/web/src/lib/codebase-input.ts @@ -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 }; +} diff --git a/packages/web/src/routes/ChatPage.tsx b/packages/web/src/routes/ChatPage.tsx index b1179d75..dde3c8c0 100644 --- a/packages/web/src/routes/ChatPage.tsx +++ b/packages/web/src/routes/ChatPage.tsx @@ -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'; @@ -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); diff --git a/packages/web/src/routes/SettingsPage.tsx b/packages/web/src/routes/SettingsPage.tsx index 780d423b..9ff8c330 100644 --- a/packages/web/src/routes/SettingsPage.tsx +++ b/packages/web/src/routes/SettingsPage.tsx @@ -11,6 +11,7 @@ import { listCodebases, listProviders, addCodebase, + getCodebaseInput, deleteCodebase, updateAssistantConfig, getCodebaseEnvVars, @@ -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(null); @@ -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); }, }); @@ -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()); } } @@ -339,11 +340,11 @@ function ProjectsSection(): React.ReactElement { {showAdd ? (
{ - setAddPath(e.target.value); + setAddValue(e.target.value); }} - placeholder="/path/to/repository" + placeholder="GitHub URL or local path" className="flex-1" />