diff --git a/packages/app/src/components/dialog-new-project.tsx b/packages/app/src/components/dialog-new-project.tsx new file mode 100644 index 000000000000..aa7e8010a6d4 --- /dev/null +++ b/packages/app/src/components/dialog-new-project.tsx @@ -0,0 +1,208 @@ +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { Dialog } from "@opencode-ai/ui/dialog" +import { TextField } from "@opencode-ai/ui/text-field" +import { ButtonV2 } from "@opencode-ai/ui/v2/components/button-v2.jsx" +import { Icon as IconV2 } from "@opencode-ai/ui/v2/components/icon.jsx" +import { createSignal, Show } from "solid-js" +import { createStore } from "solid-js/store" +import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" +import { useServer } from "@/context/server" +import { authTokenFromCredentials } from "@/utils/server" + +type Tab = "github" | "folder" + +export function DialogNewProject(props: { + onOpenSession: (directory: string) => void + onSelectDirectory: () => void +}) { + const dialog = useDialog() + const platform = usePlatform() + const language = useLanguage() + const server = useServer() + const [tab, setTab] = createSignal("github") + const [store, setStore] = createStore({ + url: "", + destination: "", + cloning: false, + error: "" as string | undefined, + }) + + async function pickDestination() { + if (platform.openDirectoryPickerDialog && server.isLocal()) { + const result = await platform.openDirectoryPickerDialog({ + title: language.t("dialog.newProject.destination.pick"), + multiple: false, + }) + const dir = Array.isArray(result) ? result[0] : result + if (dir) setStore("destination", dir) + } + } + + async function handleClone(e: SubmitEvent) { + e.preventDefault() + const url = store.url.trim() + const destination = store.destination.trim() + if (!url) return + if (!destination) return + + setStore("cloning", true) + setStore("error", undefined) + + try { + const conn = server.current + if (!conn) { + setStore("error", language.t("dialog.newProject.error.cloneFailed")) + return + } + const baseUrl = conn.http.url.replace(/\/+$/, "") + const headers: Record = { + "Content-Type": "application/json", + } + if (conn.http.password) { + headers.Authorization = `Basic ${authTokenFromCredentials({ + username: conn.http.username, + password: conn.http.password, + })}` + } + const response = await (platform.fetch ?? globalThis.fetch)(`${baseUrl}/project/clone`, { + method: "POST", + headers, + body: JSON.stringify({ url, destination }), + }) + if (!response.ok) { + const body = await response.json().catch(() => ({})) + const message = + (body as { data?: { message?: string } }).data?.message ?? + (body as { message?: string }).message ?? + response.statusText + setStore("error", message) + return + } + const raw = (await response.json()) as { directory?: string } + if (raw.directory) { + dialog.close() + props.onOpenSession(raw.directory) + } else { + setStore("error", language.t("dialog.newProject.error.cloneFailed")) + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err) + setStore("error", message) + } finally { + setStore("cloning", false) + } + } + + function handleFolderImport() { + dialog.close() + props.onSelectDirectory() + } + + return ( + +
+
+ + +
+ + +
+ setStore("url", v)} + spellcheck={false} + /> + +
+ +
+ setStore("destination", v)} + spellcheck={false} + /> + + + {language.t("dialog.newProject.destination.browse")} + + +
+
+ + + {(error) => ( +
+ {error()} +
+ )} +
+ +
+ dialog.close()} type="button"> + {language.t("common.cancel")} + + + {store.cloning ? language.t("dialog.newProject.cloning") : language.t("dialog.newProject.clone")} + +
+ +
+ + +
+
+ +
+
+
+ {language.t("dialog.newProject.folder.title")} +
+
+ {language.t("dialog.newProject.folder.description")} +
+
+ + {language.t("dialog.newProject.folder.action")} + +
+
+
+
+ ) +} diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 5d124024a3b2..cb062bd0acdd 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -320,6 +320,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "سكريبت بدء تشغيل مساحة العمل", "dialog.project.edit.worktree.startup.description": "يتم تشغيله بعد إنشاء مساحة عمل جديدة (شجرة عمل).", "dialog.project.edit.worktree.startup.placeholder": "مثال: bun install", + + "dialog.newProject.title": "مشروع جديد", + "dialog.newProject.tab.github": "استيراد من GitHub", + "dialog.newProject.tab.folder": "استيراد من مجلد", + "dialog.newProject.url.label": "رابط المستودع", + "dialog.newProject.destination.label": "وجهة الاستنساخ", + "dialog.newProject.destination.placeholder": "اختر مجلد الوجهة", + "dialog.newProject.destination.browse": "تصفح", + "dialog.newProject.destination.pick": "اختر وجهة الاستنساخ", + "dialog.newProject.clone": "استنساخ المستودع", + "dialog.newProject.cloning": "جارٍ الاستنساخ...", + "dialog.newProject.error.cloneFailed": "فشل استنساخ المستودع", + "dialog.newProject.folder.title": "فتح مشروع موجود", + "dialog.newProject.folder.description": "اختر مجلدًا محليًا لإضافته كمشروع في OpenCode.", + "dialog.newProject.folder.action": "اختر مجلد", + "context.breakdown.title": "تفصيل السياق", "context.breakdown.note": 'تفصيل تقريبي لرموز الإدخال. يشمل "أخرى" تعريفات الأدوات والنفقات العامة.', "context.breakdown.system": "النظام", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 7c6965e330c3..09374865c885 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -320,6 +320,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "Script de inicialização do espaço de trabalho", "dialog.project.edit.worktree.startup.description": "Executa após criar um novo espaço de trabalho (worktree).", "dialog.project.edit.worktree.startup.placeholder": "ex: bun install", + + "dialog.newProject.title": "Novo projeto", + "dialog.newProject.tab.github": "Importar do GitHub", + "dialog.newProject.tab.folder": "Importar de uma pasta", + "dialog.newProject.url.label": "URL do repositório", + "dialog.newProject.destination.label": "Destino do clone", + "dialog.newProject.destination.placeholder": "Selecione uma pasta de destino", + "dialog.newProject.destination.browse": "Procurar", + "dialog.newProject.destination.pick": "Escolher destino do clone", + "dialog.newProject.clone": "Clonar repositório", + "dialog.newProject.cloning": "Clonando...", + "dialog.newProject.error.cloneFailed": "Falha ao clonar o repositório", + "dialog.newProject.folder.title": "Abrir um projeto existente", + "dialog.newProject.folder.description": "Escolha uma pasta local para adicionar como projeto no OpenCode.", + "dialog.newProject.folder.action": "Escolher pasta", + "context.breakdown.title": "Detalhamento do Contexto", "context.breakdown.note": 'Detalhamento aproximado dos tokens de entrada. "Outros" inclui definições de ferramentas e overhead.', diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 201b6ed81c12..b5c561997638 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -350,6 +350,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Pokreće se nakon kreiranja novog radnog prostora (worktree).", "dialog.project.edit.worktree.startup.placeholder": "npr. bun install", + "dialog.newProject.title": "Novi projekat", + "dialog.newProject.tab.github": "Uvezi iz GitHub-a", + "dialog.newProject.tab.folder": "Uvezi iz fascikle", + "dialog.newProject.url.label": "URL repozitorija", + "dialog.newProject.destination.label": "Destinacija kloniranja", + "dialog.newProject.destination.placeholder": "Izaberite odredišnu fasciklu", + "dialog.newProject.destination.browse": "Pregledaj", + "dialog.newProject.destination.pick": "Izaberite destinaciju kloniranja", + "dialog.newProject.clone": "Kloniraj repozitorij", + "dialog.newProject.cloning": "Kloniranje...", + "dialog.newProject.error.cloneFailed": "Neuspješno kloniranje repozitorija", + "dialog.newProject.folder.title": "Otvori postojeći projekat", + "dialog.newProject.folder.description": "Izaberite lokalnu fasciklu da dodate kao projekat u OpenCode.", + "dialog.newProject.folder.action": "Izaberite fasciklu", + "context.breakdown.title": "Razlaganje konteksta", "context.breakdown.note": 'Približna raspodjela ulaznih tokena. "Ostalo" uključuje definicije alata i dodatni overhead.', diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index d52aca823eea..141223450beb 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -348,6 +348,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "Opstartsscript for arbejdsområde", "dialog.project.edit.worktree.startup.description": "Køres efter oprettelse af et nyt arbejdsområde (worktree).", "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", + + "dialog.newProject.title": "Nyt projekt", + "dialog.newProject.tab.github": "Importer fra GitHub", + "dialog.newProject.tab.folder": "Importer fra mappe", + "dialog.newProject.url.label": "Repository-URL", + "dialog.newProject.destination.label": "Klon-destination", + "dialog.newProject.destination.placeholder": "Vælg en destinationsmappe", + "dialog.newProject.destination.browse": "Gennemse", + "dialog.newProject.destination.pick": "Vælg klon-destination", + "dialog.newProject.clone": "Klon repository", + "dialog.newProject.cloning": "Kloner...", + "dialog.newProject.error.cloneFailed": "Kunne ikke klone repository", + "dialog.newProject.folder.title": "Åbn et eksisterende projekt", + "dialog.newProject.folder.description": "Vælg en lokal mappe for at tilføje den som et projekt i OpenCode.", + "dialog.newProject.folder.action": "Vælg mappe", + "context.breakdown.title": "Kontekstfordeling", "context.breakdown.note": 'Omtrentlig fordeling af input-tokens. "Andre" inkluderer værktøjsdefinitioner og overhead.', diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 4d5a64990287..5fc87b6da6ea 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -327,6 +327,22 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Wird nach dem Erstellen eines neuen Arbeitsbereichs (Worktree) ausgeführt.", "dialog.project.edit.worktree.startup.placeholder": "z. B. bun install", + + "dialog.newProject.title": "Neues Projekt", + "dialog.newProject.tab.github": "Von GitHub importieren", + "dialog.newProject.tab.folder": "Aus Ordner importieren", + "dialog.newProject.url.label": "Repository-URL", + "dialog.newProject.destination.label": "Klon-Ziel", + "dialog.newProject.destination.placeholder": "Zielordner auswählen", + "dialog.newProject.destination.browse": "Durchsuchen", + "dialog.newProject.destination.pick": "Klon-Ziel auswählen", + "dialog.newProject.clone": "Repository klonen", + "dialog.newProject.cloning": "Klone...", + "dialog.newProject.error.cloneFailed": "Repository konnte nicht geklont werden", + "dialog.newProject.folder.title": "Bestehendes Projekt öffnen", + "dialog.newProject.folder.description": "Wähle einen lokalen Ordner, um ihn als Projekt in OpenCode hinzuzufügen.", + "dialog.newProject.folder.action": "Ordner auswählen", + "context.breakdown.title": "Kontext-Aufschlüsselung", "context.breakdown.note": 'Ungefähre Aufschlüsselung der Eingabe-Token. "Andere" beinhaltet Werkzeugdefinitionen und Overhead.', diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 29f662f73270..1f4859a04b36 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -363,6 +363,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", "dialog.project.edit.worktree.startup.placeholder": "e.g. bun install", + "dialog.newProject.title": "New project", + "dialog.newProject.tab.github": "Import from GitHub", + "dialog.newProject.tab.folder": "Import from folder", + "dialog.newProject.url.label": "Repository URL", + "dialog.newProject.destination.label": "Clone destination", + "dialog.newProject.destination.placeholder": "Select a destination folder", + "dialog.newProject.destination.browse": "Browse", + "dialog.newProject.destination.pick": "Choose clone destination", + "dialog.newProject.clone": "Clone repository", + "dialog.newProject.cloning": "Cloning...", + "dialog.newProject.error.cloneFailed": "Failed to clone repository", + "dialog.newProject.folder.title": "Open an existing project", + "dialog.newProject.folder.description": "Choose a local folder to add as a project in OpenCode.", + "dialog.newProject.folder.action": "Choose folder", + "dialog.releaseNotes.action.getStarted": "Get started", "dialog.releaseNotes.action.next": "Next", "dialog.releaseNotes.action.hideFuture": "Don't show these in the future", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index b71fe03e8787..0fb305687835 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -350,6 +350,21 @@ export const dict = { "Se ejecuta después de crear un nuevo espacio de trabajo (árbol de trabajo).", "dialog.project.edit.worktree.startup.placeholder": "p. ej. bun install", + "dialog.newProject.title": "Nuevo proyecto", + "dialog.newProject.tab.github": "Importar desde GitHub", + "dialog.newProject.tab.folder": "Importar desde carpeta", + "dialog.newProject.url.label": "URL del repositorio", + "dialog.newProject.destination.label": "Destino de la clonación", + "dialog.newProject.destination.placeholder": "Selecciona una carpeta de destino", + "dialog.newProject.destination.browse": "Examinar", + "dialog.newProject.destination.pick": "Elegir destino de clonación", + "dialog.newProject.clone": "Clonar repositorio", + "dialog.newProject.cloning": "Clonando...", + "dialog.newProject.error.cloneFailed": "Error al clonar el repositorio", + "dialog.newProject.folder.title": "Abrir un proyecto existente", + "dialog.newProject.folder.description": "Elige una carpeta local para añadirla como proyecto en OpenCode.", + "dialog.newProject.folder.action": "Elegir carpeta", + "context.breakdown.title": "Desglose de Contexto", "context.breakdown.note": 'Desglose aproximado de tokens de entrada. "Otro" incluye definiciones de herramientas y sobrecarga.', diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 385dbdddefcb..8d111aa2a9db 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -322,6 +322,22 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "S'exécute après la création d'un nouvel espace de travail (arbre de travail).", "dialog.project.edit.worktree.startup.placeholder": "p. ex. bun install", + + "dialog.newProject.title": "Nouveau projet", + "dialog.newProject.tab.github": "Importer depuis GitHub", + "dialog.newProject.tab.folder": "Importer depuis un dossier", + "dialog.newProject.url.label": "URL du dépôt", + "dialog.newProject.destination.label": "Destination du clone", + "dialog.newProject.destination.placeholder": "Sélectionner un dossier de destination", + "dialog.newProject.destination.browse": "Parcourir", + "dialog.newProject.destination.pick": "Choisir la destination du clone", + "dialog.newProject.clone": "Cloner le dépôt", + "dialog.newProject.cloning": "Clonage en cours...", + "dialog.newProject.error.cloneFailed": "Échec du clonage du dépôt", + "dialog.newProject.folder.title": "Ouvrir un projet existant", + "dialog.newProject.folder.description": "Choisissez un dossier local à ajouter comme projet dans OpenCode.", + "dialog.newProject.folder.action": "Choisir un dossier", + "context.breakdown.title": "Répartition du contexte", "context.breakdown.note": "Répartition approximative des jetons d'entrée. \"Autre\" inclut les définitions d'outils et les frais généraux.", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 34ac398bd5e8..3a25bad428e6 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -320,6 +320,22 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "新しいワークスペース (ワークツリー) を作成した後に実行されます。", "dialog.project.edit.worktree.startup.placeholder": "例: bun install", + + "dialog.newProject.title": "新規プロジェクト", + "dialog.newProject.tab.github": "GitHubからインポート", + "dialog.newProject.tab.folder": "フォルダからインポート", + "dialog.newProject.url.label": "リポジトリURL", + "dialog.newProject.destination.label": "クローン先", + "dialog.newProject.destination.placeholder": "保存先フォルダを選択", + "dialog.newProject.destination.browse": "参照", + "dialog.newProject.destination.pick": "クローン先を選択", + "dialog.newProject.clone": "リポジトリをクローン", + "dialog.newProject.cloning": "クローン中...", + "dialog.newProject.error.cloneFailed": "リポジトリのクローンに失敗しました", + "dialog.newProject.folder.title": "既存のプロジェクトを開く", + "dialog.newProject.folder.description": "OpenCode にプロジェクトとして追加するローカルフォルダを選択してください。", + "dialog.newProject.folder.action": "フォルダを選択", + "context.breakdown.title": "コンテキストの内訳", "context.breakdown.note": '入力トークンのおおよその内訳です。"その他"にはツールの定義やオーバーヘッドが含まれます。', "context.breakdown.system": "システム", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index f1926f3dd28d..818295ab3a0d 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -319,6 +319,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "작업 공간 시작 스크립트", "dialog.project.edit.worktree.startup.description": "새 작업 공간(작업 트리)을 만든 뒤 실행됩니다.", "dialog.project.edit.worktree.startup.placeholder": "예: bun install", + + "dialog.newProject.title": "새 프로젝트", + "dialog.newProject.tab.github": "GitHub에서 가져오기", + "dialog.newProject.tab.folder": "폴더에서 가져오기", + "dialog.newProject.url.label": "저장소 URL", + "dialog.newProject.destination.label": "복제 대상", + "dialog.newProject.destination.placeholder": "대상 폴더 선택", + "dialog.newProject.destination.browse": "찾아보기", + "dialog.newProject.destination.pick": "복제 대상 선택", + "dialog.newProject.clone": "저장소 복제", + "dialog.newProject.cloning": "복제 중...", + "dialog.newProject.error.cloneFailed": "저장소 복제에 실패했습니다", + "dialog.newProject.folder.title": "기존 프로젝트 열기", + "dialog.newProject.folder.description": "OpenCode에 프로젝트로 추가할 로컬 폴더를 선택하세요.", + "dialog.newProject.folder.action": "폴더 선택", + "context.breakdown.title": "컨텍스트 분석", "context.breakdown.note": '입력 토큰의 대략적인 분석입니다. "기타"에는 도구 정의 및 오버헤드가 포함됩니다.', "context.breakdown.system": "시스템", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index b4adebdb7ce7..dbe2a6855e4c 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -352,6 +352,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Kjører etter at et nytt arbeidsområde (worktree) er opprettet.", "dialog.project.edit.worktree.startup.placeholder": "f.eks. bun install", + "dialog.newProject.title": "Nytt prosjekt", + "dialog.newProject.tab.github": "Importer fra GitHub", + "dialog.newProject.tab.folder": "Importer fra mappe", + "dialog.newProject.url.label": "URL for depot", + "dialog.newProject.destination.label": "Kloningsmål", + "dialog.newProject.destination.placeholder": "Velg en målmappe", + "dialog.newProject.destination.browse": "Bla gjennom", + "dialog.newProject.destination.pick": "Velg kloningsmål", + "dialog.newProject.clone": "Klon depot", + "dialog.newProject.cloning": "Kloner...", + "dialog.newProject.error.cloneFailed": "Kunne ikke klone depotet", + "dialog.newProject.folder.title": "Åpne et eksisterende prosjekt", + "dialog.newProject.folder.description": "Velg en lokal mappe for å legge til som et prosjekt i OpenCode.", + "dialog.newProject.folder.action": "Velg mappe", + "context.breakdown.title": "Kontekstfordeling", "context.breakdown.note": 'Omtrentlig fordeling av input-tokens. "Annet" inkluderer verktøydefinisjoner og overhead.', "context.breakdown.system": "System", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 02637e574f11..ca40a402d8f9 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -321,6 +321,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "Skrypt uruchamiania przestrzeni roboczej", "dialog.project.edit.worktree.startup.description": "Runs after creating a new workspace (worktree).", "dialog.project.edit.worktree.startup.placeholder": "np. bun install", + + "dialog.newProject.title": "Nowy projekt", + "dialog.newProject.tab.github": "Importuj z GitHub", + "dialog.newProject.tab.folder": "Importuj z folderu", + "dialog.newProject.url.label": "URL repozytorium", + "dialog.newProject.destination.label": "Miejsce klonowania", + "dialog.newProject.destination.placeholder": "Wybierz folder docelowy", + "dialog.newProject.destination.browse": "Przeglądaj", + "dialog.newProject.destination.pick": "Wybierz miejsce klonowania", + "dialog.newProject.clone": "Klonuj repozytorium", + "dialog.newProject.cloning": "Klonowanie...", + "dialog.newProject.error.cloneFailed": "Nie udało się sklonować repozytorium", + "dialog.newProject.folder.title": "Otwórz istniejący projekt", + "dialog.newProject.folder.description": "Wybierz folder lokalny, aby dodać go jako projekt w OpenCode.", + "dialog.newProject.folder.action": "Wybierz folder", + "context.breakdown.title": "Podział kontekstu", "context.breakdown.note": 'Przybliżony podział tokenów wejściowych. "Inne" obejmuje definicje narzędzi i narzut.', "context.breakdown.system": "System", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index a965acf278de..522d35f8d6a0 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -350,6 +350,22 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Запускается после создания нового рабочего пространства (worktree).", "dialog.project.edit.worktree.startup.placeholder": "например, bun install", + + "dialog.newProject.title": "Новый проект", + "dialog.newProject.tab.github": "Импортировать из GitHub", + "dialog.newProject.tab.folder": "Импортировать из папки", + "dialog.newProject.url.label": "URL репозитория", + "dialog.newProject.destination.label": "Место клонирования", + "dialog.newProject.destination.placeholder": "Выберите целевую папку", + "dialog.newProject.destination.browse": "Обзор", + "dialog.newProject.destination.pick": "Выберите место клонирования", + "dialog.newProject.clone": "Клонировать репозиторий", + "dialog.newProject.cloning": "Клонирование...", + "dialog.newProject.error.cloneFailed": "Не удалось клонировать репозиторий", + "dialog.newProject.folder.title": "Открыть существующий проект", + "dialog.newProject.folder.description": "Выберите локальную папку, чтобы добавить её как проект в OpenCode.", + "dialog.newProject.folder.action": "Выбрать папку", + "context.breakdown.title": "Разбивка контекста", "context.breakdown.note": 'Приблизительная разбивка входных токенов. "Другое" включает определения инструментов и накладные расходы.', diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 00df86a34d09..3cd82d2fc58b 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -349,6 +349,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "ทำงานหลังจากสร้างพื้นที่ทำงานใหม่ (worktree)", "dialog.project.edit.worktree.startup.placeholder": "เช่น bun install", + "dialog.newProject.title": "โปรเจกต์ใหม่", + "dialog.newProject.tab.github": "นำเข้าจาก GitHub", + "dialog.newProject.tab.folder": "นำเข้าจากโฟลเดอร์", + "dialog.newProject.url.label": "URL ที่เก็บข้อมูล", + "dialog.newProject.destination.label": "ปลายทางโคลน", + "dialog.newProject.destination.placeholder": "เลือกโฟลเดอร์ปลายทาง", + "dialog.newProject.destination.browse": "เรียกดู", + "dialog.newProject.destination.pick": "เลือกปลายทางโคลน", + "dialog.newProject.clone": "โคลนที่เก็บข้อมูล", + "dialog.newProject.cloning": "กำลังโคลน...", + "dialog.newProject.error.cloneFailed": "โคลนที่เก็บข้อมูลไม่สำเร็จ", + "dialog.newProject.folder.title": "เปิดโปรเจกต์ที่มีอยู่", + "dialog.newProject.folder.description": "เลือกโฟลเดอร์ในเครื่องเพื่อเพิ่มเป็นโปรเจกต์ใน OpenCode", + "dialog.newProject.folder.action": "เลือกโฟลเดอร์", + "context.breakdown.title": "การแบ่งบริบท", "context.breakdown.note": 'การแบ่งโดยประมาณของโทเค็นนำเข้า "อื่น ๆ" รวมถึงคำนิยามเครื่องมือและโอเวอร์เฮด', "context.breakdown.system": "ระบบ", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 27a6e03e367c..fc316307ce1b 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -354,6 +354,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Yeni bir çalışma alanı (worktree) oluşturduktan sonra çalışır.", "dialog.project.edit.worktree.startup.placeholder": "örneğin bun install", + "dialog.newProject.title": "Yeni proje", + "dialog.newProject.tab.github": "GitHub'dan içe aktar", + "dialog.newProject.tab.folder": "Klasörden içe aktar", + "dialog.newProject.url.label": "Depo URL'si", + "dialog.newProject.destination.label": "Klon hedefi", + "dialog.newProject.destination.placeholder": "Hedef klasör seçin", + "dialog.newProject.destination.browse": "Gözat", + "dialog.newProject.destination.pick": "Klon hedefini seçin", + "dialog.newProject.clone": "Depoyu klonla", + "dialog.newProject.cloning": "Klonlanıyor...", + "dialog.newProject.error.cloneFailed": "Depo klonlanamadı", + "dialog.newProject.folder.title": "Mevcut bir projeyi aç", + "dialog.newProject.folder.description": "OpenCode'da proje olarak eklemek için bir yerel klasör seçin.", + "dialog.newProject.folder.action": "Klasör seç", + "context.breakdown.title": "Bağlam Dökümü", "context.breakdown.note": 'Girdi tokenlerinin yaklaşık dökümü. "Diğer" araç tanımları ve ek yükleri içerir.', "context.breakdown.system": "Sistem", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 948c9a6cd449..e858c8402f54 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -363,6 +363,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "Виконується після створення нової робочої області (worktree).", "dialog.project.edit.worktree.startup.placeholder": "напр. bun install", + "dialog.newProject.title": "Новий проєкт", + "dialog.newProject.tab.github": "Імпортувати з GitHub", + "dialog.newProject.tab.folder": "Імпортувати з папки", + "dialog.newProject.url.label": "URL репозиторію", + "dialog.newProject.destination.label": "Місце клонування", + "dialog.newProject.destination.placeholder": "Оберіть цільову папку", + "dialog.newProject.destination.browse": "Огляд", + "dialog.newProject.destination.pick": "Оберіть місце клонування", + "dialog.newProject.clone": "Клонувати репозиторій", + "dialog.newProject.cloning": "Клонування...", + "dialog.newProject.error.cloneFailed": "Не вдалося клонувати репозиторій", + "dialog.newProject.folder.title": "Відкрити наявний проєкт", + "dialog.newProject.folder.description": "Оберіть локальну папку, щоб додати її як проєкт у OpenCode.", + "dialog.newProject.folder.action": "Обрати папку", + "dialog.releaseNotes.action.getStarted": "Розпочати", "dialog.releaseNotes.action.next": "Далі", "dialog.releaseNotes.action.hideFuture": "Не показувати це в майбутньому", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index f5c962a990c4..83789696432b 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -367,6 +367,21 @@ export const dict = { "dialog.project.edit.worktree.startup.description": "在创建新的工作区 (worktree) 后运行。", "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", + "dialog.newProject.title": "新建项目", + "dialog.newProject.tab.github": "从 GitHub 导入", + "dialog.newProject.tab.folder": "从文件夹导入", + "dialog.newProject.url.label": "仓库 URL", + "dialog.newProject.destination.label": "克隆目标", + "dialog.newProject.destination.placeholder": "选择目标文件夹", + "dialog.newProject.destination.browse": "浏览", + "dialog.newProject.destination.pick": "选择克隆目标", + "dialog.newProject.clone": "克隆仓库", + "dialog.newProject.cloning": "克隆中...", + "dialog.newProject.error.cloneFailed": "克隆仓库失败", + "dialog.newProject.folder.title": "打开现有项目", + "dialog.newProject.folder.description": "选择一个本地文件夹作为项目添加到 OpenCode。", + "dialog.newProject.folder.action": "选择文件夹", + "context.breakdown.title": "上下文拆分", "context.breakdown.note": "输入 token 的大致拆分。“其他”包含工具定义和开销。", "context.breakdown.system": "系统", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index edd6f7bc0647..046391ec6269 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -348,6 +348,22 @@ export const dict = { "dialog.project.edit.worktree.startup": "工作區啟動腳本", "dialog.project.edit.worktree.startup.description": "在建立新的工作區 (worktree) 後執行。", "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", + + "dialog.newProject.title": "新增專案", + "dialog.newProject.tab.github": "從 GitHub 匯入", + "dialog.newProject.tab.folder": "從資料夾匯入", + "dialog.newProject.url.label": "儲存庫 URL", + "dialog.newProject.destination.label": "克隆目標", + "dialog.newProject.destination.placeholder": "選擇目標資料夾", + "dialog.newProject.destination.browse": "瀏覽", + "dialog.newProject.destination.pick": "選擇克隆目標", + "dialog.newProject.clone": "克隆儲存庫", + "dialog.newProject.cloning": "克隆中...", + "dialog.newProject.error.cloneFailed": "克隆儲存庫失敗", + "dialog.newProject.folder.title": "開啟現有專案", + "dialog.newProject.folder.description": "選擇一個本地資料夾作為專案加入 OpenCode。", + "dialog.newProject.folder.action": "選擇資料夾", + "context.breakdown.title": "上下文拆分", "context.breakdown.note": "輸入 token 的大致拆分。「其他」包含工具定義和額外開銷。", "context.breakdown.system": "系統", diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 8e31ac3913e7..97acf8da1f74 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -18,6 +18,7 @@ import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" import { useDialog } from "@opencode-ai/ui/context/dialog" import { DialogSelectDirectory } from "@/components/dialog-select-directory" +import { DialogNewProject } from "@/components/dialog-new-project" import { DialogSelectServer } from "@/components/dialog-select-server" import { ServerConnection, useServer } from "@/context/server" import { useServerSync } from "@/context/server-sync" @@ -129,6 +130,18 @@ function HomeDesign() { setState("project", directory) } + function showNewProjectDialog() { + dialog.show( + () => ( + void chooseProject()} + /> + ), + () => {}, + ) + } + function openNewSession() { const project = selectedProject() if (!project) { @@ -205,6 +218,7 @@ function HomeDesign() { selectProject={selectProject} openNewSession={openProjectNewSession} chooseProject={() => void chooseProject()} + showNewProjectDialog={showNewProjectDialog} editProject={showEditProjectDialog} closeProject={(directory) => { layout.projects.close(directory) @@ -229,7 +243,7 @@ function HomeDesign() { title={language.t("home.empty.title")} description={language.t("home.empty.description")} action={language.t("home.project.add")} - onAction={() => void chooseProject()} + onAction={showNewProjectDialog} /> } > @@ -288,6 +302,7 @@ function HomeProjectColumn(props: { selectProject: (directory: string) => void openNewSession: (directory: string) => void chooseProject: () => void + showNewProjectDialog: () => void editProject: (project: LocalProject) => void closeProject: (directory: string) => void clearNotifications: (project: LocalProject) => void @@ -309,7 +324,7 @@ function HomeProjectColumn(props: { size="large" class="titlebar-icon [&_[data-slot=icon-svg]]:text-v2-icon-icon-muted" icon={} - onClick={props.chooseProject} + onClick={props.showNewProjectDialog} aria-label={props.language.t("home.project.add")} /> diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 2ef299e94452..c370674b81fa 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -110,6 +110,11 @@ export class NotFoundError extends Schema.TaggedErrorClass()("Pro projectID: ProjectID, }) {} +export class CloneError extends Schema.TaggedErrorClass()("Project.CloneError", { + url: Schema.String, + message: Schema.String, +}) {} + // --------------------------------------------------------------------------- // Effect service // --------------------------------------------------------------------------- @@ -127,6 +132,7 @@ export interface Interface { readonly get: (id: ProjectID) => Effect.Effect readonly update: (input: UpdateInput) => Effect.Effect readonly initGit: (input: { directory: string; project: Info }) => Effect.Effect + readonly clone: (input: { url: string; destination: string }) => Effect.Effect readonly setInitialized: (id: ProjectID) => Effect.Effect readonly sandboxes: (id: ProjectID) => Effect.Effect readonly addSandbox: (id: ProjectID, directory: string) => Effect.Effect @@ -137,6 +143,18 @@ export class Service extends Context.Service()("@opencode/Pr type GitResult = { code: number; text: string; stderr: string } +function deriveRepoName(url: string): string | undefined { + const withoutParams = url.replace(/[?#].*$/, "") + const trimmed = withoutParams.replace(/\/+$/, "").replace(/\.git$/, "") + const lastSegment = trimmed.split("/").pop() + if (!lastSegment) return undefined + if (lastSegment.includes(":")) { + const afterColon = lastSegment.split(":").pop() + return afterColon || undefined + } + return lastSegment +} + export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -394,6 +412,29 @@ export const layer = Layer.effect( return project }) + const clone = Effect.fn("Project.clone")(function* (input: { url: string; destination: string }) { + if (!(yield* Effect.sync(() => which("git")))) { + return yield* new CloneError({ url: input.url, message: "Git is not installed" }) + } + const repoName = deriveRepoName(input.url) + const cloneTarget = repoName + ? input.destination.replace(/\/+$/, "") + "/" + repoName + : input.destination + const cloneExists = yield* fs.exists(cloneTarget).pipe(Effect.orDie) + if (cloneExists) { + return yield* new CloneError({ + url: input.url, + message: `Target already exists: ${cloneTarget}`, + }) + } + const result = yield* git(["clone", input.url, cloneTarget]) + if (result.code !== 0) { + const msg = result.stderr.trim() || result.text.trim() || "Failed to clone repository" + return yield* new CloneError({ url: input.url, message: msg }) + } + return cloneTarget + }) + const setInitialized = Effect.fn("Project.setInitialized")(function* (id: ProjectID) { yield* db((d) => d.update(ProjectTable).set({ time_initialized: Date.now() }).where(eq(ProjectTable.id, id)).run(), @@ -471,6 +512,7 @@ export const layer = Layer.effect( get, update, initGit, + clone, setInitialized, sandboxes, addSandbox, diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts index b7be4044fc0e..8096a84bd247 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts @@ -9,6 +9,12 @@ import { WorkspaceRoutingMiddleware, WorkspaceRoutingQuery } from "../middleware import { described } from "./metadata" const root = "/project" + +const ClonePayload = Schema.Struct({ + url: Schema.String.annotate({ description: "Git repository URL to clone" }), + destination: Schema.String.annotate({ description: "Local directory path to clone into" }), +}) + const UpdatePayload = Schema.Struct({ name: Schema.optional(Schema.String), icon: Schema.optional(Project.Info.fields.icon), @@ -49,6 +55,18 @@ export const ProjectApi = HttpApi.make("project") description: "Create a git repository for the current project and return the refreshed project info.", }), ), + HttpApiEndpoint.post("clone", `${root}/clone`, { + query: WorkspaceRoutingQuery, + payload: ClonePayload, + success: described(Schema.Struct({ directory: Schema.String }), "Cloned directory path"), + error: [HttpApiError.BadRequest], + }).annotateMerge( + OpenApi.annotations({ + identifier: "project.clone", + summary: "Clone a repository", + description: "Clone a git repository from a URL to a local destination directory.", + }), + ), HttpApiEndpoint.patch("update", `${root}/:projectID`, { params: { projectID: ProjectID }, query: WorkspaceRoutingQuery, diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts index 1b61204c4ca0..ef3ce2b88155 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/project.ts @@ -4,7 +4,7 @@ import { ProjectID } from "@/project/schema" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { InstanceHttpApi } from "../api" -import { ProjectNotFoundError } from "../errors" +import { InvalidRequestError, ProjectNotFoundError } from "../errors" import { markInstanceForReload } from "../lifecycle" export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) => @@ -32,6 +32,17 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", return next }) + const clone = Effect.fn("ProjectHttpApi.clone")(function* (ctx: { + payload: { url: string; destination: string } + }) { + const directory = yield* svc.clone({ url: ctx.payload.url, destination: ctx.payload.destination }).pipe( + Effect.catchTag("Project.CloneError", (error) => + Effect.fail(new InvalidRequestError({ message: error.message })), + ), + ) + return { directory } + }) + const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: { params: { projectID: ProjectID } payload: Project.UpdatePayload @@ -48,6 +59,11 @@ export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", ) }) - return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update) + return handlers + .handle("list", list) + .handle("current", current) + .handle("initGit", initGit) + .handle("clone", clone) + .handle("update", update) }), )