Merge pull request #1110 from LocNguyenSGU/fix/issue-1108-settings-add-project-url-support

fix: accept GitHub URLs in settings add project
This commit is contained in:
DIY Smart Code 2026-04-16 23:55:51 +02:00 committed by GitHub
commit b7b445bd31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 95 additions and 26 deletions

View file

@ -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 <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.

View file

@ -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);

View file

@ -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;

View file

@ -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',
});
});
});

View file

@ -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 };
}

View file

@ -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);

View file

@ -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<string | null>(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 ? (
<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}>
@ -355,7 +356,7 @@ function ProjectsSection(): React.ReactElement {
size="sm"
onClick={() => {
setShowAdd(false);
setAddPath('');
setAddValue('');
}}
>
Cancel