mirror of
https://github.com/coleam00/Archon
synced 2026-04-21 13:37:41 +00:00
fix(web): unify Add Project URL/path classification across UI entry points
Settings → Projects Add Project only submitted { path }, so GitHub URLs
entered there failed even though the API and the Sidebar Add Project
already accepted them. Closes #1108.
Changes:
- Add packages/web/src/lib/codebase-input.ts: shared getCodebaseInput()
helper returning a discriminated { path } | { url } union (re-exported
from api.ts for convenience).
- Use the helper from all three Add Project entry points: Sidebar,
Settings, and ChatPage. Removes three divergent inline heuristics.
- SettingsPage: rename addPath → addValue (state now holds either URL
or local path) and update placeholder text.
- Tests: cover https://, git@ shorthand, ssh://, git://, whitespace,
unix/relative/home/Windows/UNC paths.
- Docs: document the unified Add Project entry point in adapters/web.md.
Heuristic flips from "assume URL unless explicitly local" to "assume
local unless explicitly remote" — only inputs starting with https?://,
ssh://, git@, or git:// are sent as { url }; everything else is sent
as { path }. The server already resolves tilde/relative paths.
Co-authored-by: Nguyen Huu Loc <lockbkbang@gmail.com>
This commit is contained in:
parent
86e4c8d605
commit
9dd57b2f3c
7 changed files with 95 additions and 26 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
64
packages/web/src/lib/codebase-input.test.ts
Normal file
64
packages/web/src/lib/codebase-input.test.ts
Normal 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
10
packages/web/src/lib/codebase-input.ts
Normal file
10
packages/web/src/lib/codebase-input.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue