Skip to content
Open
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
208 changes: 208 additions & 0 deletions packages/app/src/components/dialog-new-project.tsx
Original file line number Diff line number Diff line change
@@ -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<Tab>("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<string, string> = {
"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 (
<Dialog title={language.t("dialog.newProject.title")} class="w-full max-w-[520px] mx-auto">
<div class="flex flex-col gap-4 p-6 pt-0">
<div class="flex gap-1 rounded-lg bg-v2-background-bg-deep p-1">
<button
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-sm [font-weight:440] transition-colors"
classList={{
"bg-v2-background-bg-base text-v2-text-text-base shadow-[var(--v2-elevation-raised)]": tab() === "github",
"text-v2-text-text-muted hover:text-v2-text-text-base": tab() !== "github",
}}
onClick={() => setTab("github")}
>
<IconV2 name="git-branch" size="small" class="mr-1.5 inline-block align-text-bottom" />
{language.t("dialog.newProject.tab.github")}
</button>
<button
type="button"
class="flex-1 rounded-md px-3 py-1.5 text-sm [font-weight:440] transition-colors"
classList={{
"bg-v2-background-bg-base text-v2-text-text-base shadow-[var(--v2-elevation-raised)]": tab() === "folder",
"text-v2-text-text-muted hover:text-v2-text-text-base": tab() !== "folder",
}}
onClick={() => setTab("folder")}
>
<IconV2 name="folder" size="small" class="mr-1.5 inline-block align-text-bottom" />
{language.t("dialog.newProject.tab.folder")}
</button>
</div>

<Show when={tab() === "github"}>
<form onSubmit={handleClone} class="flex flex-col gap-4">
<TextField
autofocus
type="text"
label={language.t("dialog.newProject.url.label")}
placeholder="https://github.com/owner/repo"
value={store.url}
onChange={(v) => setStore("url", v)}
spellcheck={false}
/>

<div class="flex flex-col gap-1.5">
<label class="text-12-medium text-text-weak">{language.t("dialog.newProject.destination.label")}</label>
<div class="flex gap-2">
<TextField
type="text"
class="flex-1"
placeholder={language.t("dialog.newProject.destination.placeholder")}
value={store.destination}
onChange={(v) => setStore("destination", v)}
spellcheck={false}
/>
<Show when={platform.openDirectoryPickerDialog && server.isLocal()}>
<ButtonV2 variant="neutral" size="normal" onClick={pickDestination} type="button">
{language.t("dialog.newProject.destination.browse")}
</ButtonV2>
</Show>
</div>
</div>

<Show when={store.error}>
{(error) => (
<div class="rounded-md bg-surface-critical-base/10 px-3 py-2 text-12-regular text-text-critical-base">
{error()}
</div>
)}
</Show>

<div class="flex justify-end gap-2 pt-2">
<ButtonV2 variant="ghost" size="normal" onClick={() => dialog.close()} type="button">
{language.t("common.cancel")}
</ButtonV2>
<ButtonV2
variant="contrast"
size="normal"
type="submit"
disabled={store.cloning || !store.url.trim() || !store.destination.trim()}
>
{store.cloning ? language.t("dialog.newProject.cloning") : language.t("dialog.newProject.clone")}
</ButtonV2>
</div>
</form>
</Show>

<Show when={tab() === "folder"}>
<div class="flex flex-col items-center gap-4 py-8 text-center">
<div class="flex size-10 items-center justify-center rounded-[10px] bg-v2-background-bg-deep text-v2-icon-icon-muted shadow-[var(--v2-elevation-raised)]">
<IconV2 name="folder" />
</div>
<div class="flex max-w-[320px] flex-col gap-1">
<div class="text-v2-text-text-base [font-weight:530]">
{language.t("dialog.newProject.folder.title")}
</div>
<div class="text-v2-text-text-muted [font-weight:440]">
{language.t("dialog.newProject.folder.description")}
</div>
</div>
<ButtonV2 variant="neutral" size="normal" icon="folder-add-left" onClick={handleFolderImport}>
{language.t("dialog.newProject.folder.action")}
</ButtonV2>
</div>
</Show>
</div>
</Dialog>
)
}
16 changes: 16 additions & 0 deletions packages/app/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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": "النظام",
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/i18n/br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/i18n/bs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/i18n/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Loading
Loading