mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
feat(ui): support custom workspace names (#522)
## Summary - Adds editable workspace display names for recent folders. - Shows the saved workspace name in the recent folder list and instance tab, including the launching workspace state. - Preserves workspace names when recent folders are relaunched and keeps basename fallback for unnamed folders. Fixes #510 ## Validation - npm run typecheck --workspace @codenomad/ui - npm run typecheck --workspace @neuralnomads/codenomad
This commit is contained in:
parent
9d4c304773
commit
994157f577
15 changed files with 321 additions and 13 deletions
|
|
@ -40,6 +40,7 @@ const PreferencesSchema = z
|
|||
const RecentFolderSchema = z.object({
|
||||
path: z.string(),
|
||||
lastAccessed: z.number().nonnegative(),
|
||||
projectName: z.string().optional(),
|
||||
})
|
||||
|
||||
const OpenCodeBinarySchema = z.object({
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ const App: Component = () => {
|
|||
const { t } = useI18n()
|
||||
const {
|
||||
preferences,
|
||||
recentFolders,
|
||||
serverSettings,
|
||||
recordWorkspaceLaunch,
|
||||
toggleShowThinkingBlocks,
|
||||
|
|
@ -265,11 +266,22 @@ const App: Component = () => {
|
|||
|
||||
const launchErrorMessage = () => launchError()?.message ?? ""
|
||||
|
||||
function getPathBasename(path: string): string {
|
||||
const normalized = path.replace(/[\\/]+$/, "")
|
||||
return normalized.split(/[\\/]/).pop() || path
|
||||
}
|
||||
|
||||
function getProjectNameForFolder(folderPath: string): string {
|
||||
const recent = recentFolders().find((folder) => folder.path === folderPath)
|
||||
return recent?.projectName?.trim() || getPathBasename(folderPath)
|
||||
}
|
||||
|
||||
async function handleSelectFolder(folderPath: string, binaryPath?: string, options?: { forceNew?: boolean }) {
|
||||
if (!folderPath) {
|
||||
return
|
||||
}
|
||||
const selectedBinary = binaryPath || serverSettings().opencodeBinary || "opencode"
|
||||
const projectName = getProjectNameForFolder(folderPath)
|
||||
recordWorkspaceLaunch(folderPath, selectedBinary)
|
||||
clearLaunchError()
|
||||
|
||||
|
|
@ -283,7 +295,7 @@ const App: Component = () => {
|
|||
|
||||
setIsSelectingFolder(true)
|
||||
try {
|
||||
const instanceId = await createInstance(folderPath, selectedBinary)
|
||||
const instanceId = await createInstance(folderPath, selectedBinary, projectName)
|
||||
selectInstanceTab(instanceId)
|
||||
setShowFolderSelection(false)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Dialog } from "@kobalte/core/dialog"
|
||||
import { Select } from "@kobalte/core/select"
|
||||
import { Component, createMemo, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js"
|
||||
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X, Globe, Loader2, GitBranch } from "lucide-solid"
|
||||
import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X, Globe, Loader2, GitBranch, Pencil } from "lucide-solid"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import DirectoryBrowserDialog from "./directory-browser-dialog"
|
||||
import Kbd from "./kbd"
|
||||
import ProjectRenameDialog from "./project-rename-dialog"
|
||||
import { openNativeFolderDialog, supportsNativeDialogsInCurrentWindow } from "../lib/native/native-functions"
|
||||
import { useFolderDrop } from "../lib/hooks/use-folder-drop"
|
||||
import VersionPill from "./version-pill"
|
||||
|
|
@ -18,7 +19,7 @@ import { openExternalUrl } from "../lib/external-url"
|
|||
import { serverApi } from "../lib/api-client"
|
||||
import { canOpenRemoteWindows, isTauriHost } from "../lib/runtime-env"
|
||||
import { openRemoteServerWindow } from "../lib/native/remote-window"
|
||||
import { getExistingInstanceForFolder } from "../stores/instances"
|
||||
import { getExistingInstanceForFolder, updateProjectNameForFolder } from "../stores/instances"
|
||||
|
||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||
const GITHUB_URL = "https://github.com/NeuralNomadsAI/CodeNomad"
|
||||
|
|
@ -38,6 +39,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
const {
|
||||
recentFolders,
|
||||
removeRecentFolder,
|
||||
renameRecentFolderProject,
|
||||
preferences,
|
||||
updatePreferences,
|
||||
serverSettings,
|
||||
|
|
@ -53,6 +55,8 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
const [isFolderBrowserOpen, setIsFolderBrowserOpen] = createSignal(false)
|
||||
const [isCloneDialogOpen, setIsCloneDialogOpen] = createSignal(false)
|
||||
const [isCloneDestinationBrowserOpen, setIsCloneDestinationBrowserOpen] = createSignal(false)
|
||||
const [renameProjectTarget, setRenameProjectTarget] = createSignal<{ path: string; name: string; label: string } | null>(null)
|
||||
const [isRenamingProject, setIsRenamingProject] = createSignal(false)
|
||||
const [cloneRepositoryUrl, setCloneRepositoryUrl] = createSignal("")
|
||||
const [cloneDestinationPath, setCloneDestinationPath] = createSignal("")
|
||||
const [cleanupCloneDestination, setCleanupCloneDestination] = createSignal(false)
|
||||
|
|
@ -514,6 +518,35 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
function getProjectDisplayName(path: string, projectName?: string): string {
|
||||
const trimmedName = projectName?.trim()
|
||||
return trimmedName || splitFolderPath(path).baseName
|
||||
}
|
||||
|
||||
function openProjectRename(path: string, projectName?: string, e?: Event) {
|
||||
if (isLoading()) return
|
||||
e?.stopPropagation()
|
||||
const defaultName = getProjectDisplayName(path, projectName)
|
||||
setRenameProjectTarget({ path, name: defaultName, label: defaultName })
|
||||
}
|
||||
|
||||
function closeProjectRenameDialog() {
|
||||
setRenameProjectTarget(null)
|
||||
}
|
||||
|
||||
async function handleProjectRenameSubmit(nextName: string) {
|
||||
const target = renameProjectTarget()
|
||||
if (!target || !nextName.trim()) return
|
||||
setIsRenamingProject(true)
|
||||
try {
|
||||
await renameRecentFolderProject(target.path, nextName)
|
||||
updateProjectNameForFolder(target.path, nextName)
|
||||
setRenameProjectTarget(null)
|
||||
} finally {
|
||||
setIsRenamingProject(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getDisplayPath(path: string): string {
|
||||
if (!path) return path
|
||||
|
|
@ -914,7 +947,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
<div class="flex items-center gap-2 mb-1">
|
||||
<Folder class="w-4 h-4 flex-shrink-0 icon-muted" />
|
||||
<span class="text-sm font-medium truncate text-primary">
|
||||
{splitFolderPath(folder.path).baseName}
|
||||
{getProjectDisplayName(folder.path, folder.projectName)}
|
||||
</span>
|
||||
<Show when={existingInstance()}>
|
||||
<span class="rounded-full border border-base px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-secondary">
|
||||
|
|
@ -934,6 +967,14 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => openProjectRename(folder.path, folder.projectName, e)}
|
||||
disabled={isLoading()}
|
||||
class="p-2 transition-all hover:bg-surface-hover opacity-70 hover:opacity-100 rounded"
|
||||
title={t("folderSelection.recent.rename")}
|
||||
>
|
||||
<Pencil class="w-3.5 h-3.5 transition-colors icon-muted" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleRemove(folder.path, e)}
|
||||
disabled={isLoading()}
|
||||
|
|
@ -1089,6 +1130,15 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||
onSelect={handleBrowserSelect}
|
||||
/>
|
||||
|
||||
<ProjectRenameDialog
|
||||
open={Boolean(renameProjectTarget())}
|
||||
currentName={renameProjectTarget()?.name ?? ""}
|
||||
projectLabel={renameProjectTarget()?.label}
|
||||
isSubmitting={isRenamingProject()}
|
||||
onRename={handleProjectRenameSubmit}
|
||||
onClose={closeProjectRenameDialog}
|
||||
/>
|
||||
|
||||
<DirectoryBrowserDialog
|
||||
open={isCloneDestinationBrowserOpen()}
|
||||
title={t("folderSelection.clone.destination.title")}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
|
|||
return null
|
||||
}
|
||||
})
|
||||
const tabLabel = createMemo(() => props.instance.projectName?.trim() || getPathBasename(props.instance.folder))
|
||||
|
||||
return (
|
||||
<div class="group">
|
||||
|
|
@ -67,7 +68,7 @@ const InstanceTab: Component<InstanceTabProps> = (props) => {
|
|||
>
|
||||
<FolderOpen class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="tab-label">
|
||||
{getPathBasename(props.instance.folder)}
|
||||
{tabLabel()}
|
||||
</span>
|
||||
<Show when={statusClassName() && statusTitle()}>
|
||||
<span
|
||||
|
|
|
|||
133
packages/ui/src/components/project-rename-dialog.tsx
Normal file
133
packages/ui/src/components/project-rename-dialog.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { Dialog } from "@kobalte/core/dialog"
|
||||
import { Component, Show, createEffect, createSignal } from "solid-js"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
|
||||
interface ProjectRenameDialogProps {
|
||||
open: boolean
|
||||
currentName: string
|
||||
projectLabel?: string
|
||||
isSubmitting?: boolean
|
||||
onRename: (nextName: string) => Promise<void> | void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ProjectRenameDialog: Component<ProjectRenameDialogProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const [name, setName] = createSignal("")
|
||||
const inputId = `project-rename-${Math.random().toString(36).slice(2)}`
|
||||
let inputRef: HTMLInputElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.open) return
|
||||
setName(props.currentName ?? "")
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (!props.open) return
|
||||
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return
|
||||
window.requestAnimationFrame(() => {
|
||||
inputRef?.focus()
|
||||
inputRef?.select()
|
||||
})
|
||||
})
|
||||
|
||||
const isSubmitting = () => Boolean(props.isSubmitting)
|
||||
const isRenameDisabled = () => isSubmitting() || !name().trim()
|
||||
|
||||
async function handleRename(event?: Event) {
|
||||
event?.preventDefault()
|
||||
if (isRenameDisabled()) return
|
||||
await props.onRename(name().trim())
|
||||
}
|
||||
|
||||
const description = () => {
|
||||
if (props.projectLabel && props.projectLabel.trim()) {
|
||||
return t("projectRenameDialog.description.withLabel", { label: props.projectLabel })
|
||||
}
|
||||
return t("projectRenameDialog.description.default")
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open && !isSubmitting()) {
|
||||
props.onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<Dialog.Content class="modal-surface w-full max-w-sm p-6" tabIndex={-1}>
|
||||
<Dialog.Title class="text-lg font-semibold text-primary">{t("projectRenameDialog.title")}</Dialog.Title>
|
||||
<Dialog.Description class="text-sm text-secondary mt-1">
|
||||
{description()}
|
||||
</Dialog.Description>
|
||||
|
||||
<form class="mt-4 space-y-4" onSubmit={handleRename}>
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-secondary" for={inputId}>
|
||||
{t("projectRenameDialog.input.label")}
|
||||
</label>
|
||||
<input
|
||||
id={inputId}
|
||||
ref={(element) => {
|
||||
inputRef = element
|
||||
}}
|
||||
type="text"
|
||||
dir="auto"
|
||||
value={name()}
|
||||
onInput={(event) => setName(event.currentTarget.value)}
|
||||
placeholder={t("projectRenameDialog.input.placeholder")}
|
||||
class="w-full px-3 py-2 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary"
|
||||
onClick={() => {
|
||||
if (!isSubmitting()) {
|
||||
props.onClose()
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting()}
|
||||
>
|
||||
{t("projectRenameDialog.actions.cancel")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="button-primary flex items-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
disabled={isRenameDisabled()}
|
||||
>
|
||||
<Show
|
||||
when={!isSubmitting()}
|
||||
fallback={
|
||||
<>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{t("projectRenameDialog.actions.renaming")}</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{t("projectRenameDialog.actions.rename")}
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProjectRenameDialog
|
||||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "Recent Folders",
|
||||
"folderSelection.recent.subtitle.one": "{count} folder available",
|
||||
"folderSelection.recent.subtitle.other": "{count} folders available",
|
||||
"folderSelection.recent.rename": "Rename workspace",
|
||||
"folderSelection.recent.remove": "Remove from recent",
|
||||
"folderSelection.recent.openBadge": "Open",
|
||||
"folderSelection.recent.alreadyOpenTitle": "Project already open",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "Cancel",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad needs the local certificate to be trusted before it can open self-signed HTTPS remote windows.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Rename workspace",
|
||||
"projectRenameDialog.description.withLabel": "Update the workspace name for \"{label}\".",
|
||||
"projectRenameDialog.description.default": "Set a workspace name for this folder.",
|
||||
"projectRenameDialog.input.label": "Workspace name",
|
||||
"projectRenameDialog.input.placeholder": "Enter workspace name",
|
||||
"projectRenameDialog.actions.cancel": "Cancel",
|
||||
"projectRenameDialog.actions.rename": "Rename",
|
||||
"projectRenameDialog.actions.renaming": "Renaming...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "Carpetas recientes",
|
||||
"folderSelection.recent.subtitle.one": "{count} carpeta disponible",
|
||||
"folderSelection.recent.subtitle.other": "{count} carpetas disponibles",
|
||||
"folderSelection.recent.rename": "Renombrar workspace",
|
||||
"folderSelection.recent.remove": "Eliminar de recientes",
|
||||
"folderSelection.recent.openBadge": "Abierta",
|
||||
"folderSelection.recent.alreadyOpenTitle": "Proyecto ya abierto",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "Cancelar",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad necesita que el certificado local sea de confianza antes de poder abrir ventanas remotas HTTPS autofirmadas.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Renombrar workspace",
|
||||
"projectRenameDialog.description.withLabel": "Actualiza el nombre del workspace para \"{label}\".",
|
||||
"projectRenameDialog.description.default": "Define un nombre de workspace para esta carpeta.",
|
||||
"projectRenameDialog.input.label": "Nombre del workspace",
|
||||
"projectRenameDialog.input.placeholder": "Introduce el nombre del workspace",
|
||||
"projectRenameDialog.actions.cancel": "Cancelar",
|
||||
"projectRenameDialog.actions.rename": "Renombrar",
|
||||
"projectRenameDialog.actions.renaming": "Renombrando...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "Dossiers récents",
|
||||
"folderSelection.recent.subtitle.one": "{count} dossier disponible",
|
||||
"folderSelection.recent.subtitle.other": "{count} dossiers disponibles",
|
||||
"folderSelection.recent.rename": "Renommer l'espace de travail",
|
||||
"folderSelection.recent.remove": "Retirer des récents",
|
||||
"folderSelection.recent.openBadge": "Ouvert",
|
||||
"folderSelection.recent.alreadyOpenTitle": "Projet déjà ouvert",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "Annuler",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad a besoin que le certificat local soit approuve avant de pouvoir ouvrir des fenetres distantes HTTPS auto-signees.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Renommer l'espace de travail",
|
||||
"projectRenameDialog.description.withLabel": "Mettez à jour le nom de l'espace de travail pour \"{label}\".",
|
||||
"projectRenameDialog.description.default": "Définissez un nom d'espace de travail pour ce dossier.",
|
||||
"projectRenameDialog.input.label": "Nom de l'espace de travail",
|
||||
"projectRenameDialog.input.placeholder": "Saisissez le nom de l'espace de travail",
|
||||
"projectRenameDialog.actions.cancel": "Annuler",
|
||||
"projectRenameDialog.actions.rename": "Renommer",
|
||||
"projectRenameDialog.actions.renaming": "Renommage...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "תיקיות אחרונות",
|
||||
"folderSelection.recent.subtitle.one": "{count} תיקייה זמינה",
|
||||
"folderSelection.recent.subtitle.other": "{count} תיקיות זמינות",
|
||||
"folderSelection.recent.rename": "שנה שם סביבת עבודה",
|
||||
"folderSelection.recent.remove": "הסר מהרשימה האחרונה",
|
||||
"folderSelection.recent.openBadge": "פתוח",
|
||||
"folderSelection.recent.alreadyOpenTitle": "הפרויקט כבר פתוח",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "ביטול",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad צריך שהאישור המקומי יהיה מהימן לפני שיוכל לפתוח חלונות HTTPS מרוחקים עם אישור בחתימה עצמית.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "שנה שם סביבת עבודה",
|
||||
"projectRenameDialog.description.withLabel": "עדכן את שם סביבת העבודה עבור \"{label}\".",
|
||||
"projectRenameDialog.description.default": "הגדר שם סביבת עבודה לתיקייה זו.",
|
||||
"projectRenameDialog.input.label": "שם סביבת עבודה",
|
||||
"projectRenameDialog.input.placeholder": "הזן שם סביבת עבודה",
|
||||
"projectRenameDialog.actions.cancel": "ביטול",
|
||||
"projectRenameDialog.actions.rename": "שנה שם",
|
||||
"projectRenameDialog.actions.renaming": "משנה שם...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "最近使ったフォルダ",
|
||||
"folderSelection.recent.subtitle.one": "{count} 件のフォルダ",
|
||||
"folderSelection.recent.subtitle.other": "{count} 件のフォルダ",
|
||||
"folderSelection.recent.rename": "ワークスペース名を変更",
|
||||
"folderSelection.recent.remove": "履歴から削除",
|
||||
"folderSelection.recent.openBadge": "開いています",
|
||||
"folderSelection.recent.alreadyOpenTitle": "プロジェクトはすでに開いています",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "キャンセル",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "自己署名 HTTPS のリモートウィンドウを開くには、CodeNomad のローカル証明書を信頼する必要があります。",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "ワークスペース名を変更",
|
||||
"projectRenameDialog.description.withLabel": "\"{label}\" のワークスペース名を更新します。",
|
||||
"projectRenameDialog.description.default": "このフォルダのワークスペース名を設定します。",
|
||||
"projectRenameDialog.input.label": "ワークスペース名",
|
||||
"projectRenameDialog.input.placeholder": "ワークスペース名を入力",
|
||||
"projectRenameDialog.actions.cancel": "キャンセル",
|
||||
"projectRenameDialog.actions.rename": "名前を変更",
|
||||
"projectRenameDialog.actions.renaming": "変更中...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "Недавние папки",
|
||||
"folderSelection.recent.subtitle.one": "Доступна {count} папка",
|
||||
"folderSelection.recent.subtitle.other": "Доступно {count} папок",
|
||||
"folderSelection.recent.rename": "Переименовать рабочее пространство",
|
||||
"folderSelection.recent.remove": "Убрать из недавних",
|
||||
"folderSelection.recent.openBadge": "Открыта",
|
||||
"folderSelection.recent.alreadyOpenTitle": "Проект уже открыт",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "Отмена",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad должен доверять локальному сертификату, прежде чем сможет открывать удаленные HTTPS-окна с самоподписанным сертификатом.",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "Переименовать рабочее пространство",
|
||||
"projectRenameDialog.description.withLabel": "Обновите имя рабочего пространства для \"{label}\".",
|
||||
"projectRenameDialog.description.default": "Задайте имя рабочего пространства для этой папки.",
|
||||
"projectRenameDialog.input.label": "Имя рабочего пространства",
|
||||
"projectRenameDialog.input.placeholder": "Введите имя рабочего пространства",
|
||||
"projectRenameDialog.actions.cancel": "Отмена",
|
||||
"projectRenameDialog.actions.rename": "Переименовать",
|
||||
"projectRenameDialog.actions.renaming": "Переименование...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.recent.title": "最近的文件夹",
|
||||
"folderSelection.recent.subtitle.one": "{count} 个文件夹可用",
|
||||
"folderSelection.recent.subtitle.other": "{count} 个文件夹可用",
|
||||
"folderSelection.recent.rename": "重命名工作区",
|
||||
"folderSelection.recent.remove": "从最近列表移除",
|
||||
"folderSelection.recent.openBadge": "已打开",
|
||||
"folderSelection.recent.alreadyOpenTitle": "项目已打开",
|
||||
|
|
@ -95,4 +96,13 @@ export const folderSelectionMessages = {
|
|||
"folderSelection.servers.certificateInstall.cancelLabel": "取消",
|
||||
"folderSelection.servers.certificateInstall.cancelled": "CodeNomad 需要先信任本地证书,才能打开使用自签名 HTTPS 的远程窗口。",
|
||||
"folderSelection.sidecars.button": "Open SideCar",
|
||||
|
||||
"projectRenameDialog.title": "重命名工作区",
|
||||
"projectRenameDialog.description.withLabel": "更新“{label}”的工作区名称。",
|
||||
"projectRenameDialog.description.default": "为此文件夹设置工作区名称。",
|
||||
"projectRenameDialog.input.label": "工作区名称",
|
||||
"projectRenameDialog.input.placeholder": "输入工作区名称",
|
||||
"projectRenameDialog.actions.cancel": "取消",
|
||||
"projectRenameDialog.actions.rename": "重命名",
|
||||
"projectRenameDialog.actions.renaming": "正在重命名...",
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -108,11 +108,12 @@ const MAX_LOG_ENTRIES = 1000
|
|||
const pendingDisposeRequests = new Map<string, Promise<boolean>>()
|
||||
const pendingRehydrations = new Map<string, Promise<void>>()
|
||||
|
||||
function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor): Instance {
|
||||
function workspaceDescriptorToInstance(descriptor: WorkspaceDescriptor, projectName?: string): Instance {
|
||||
const existing = instances().get(descriptor.id)
|
||||
return {
|
||||
id: descriptor.id,
|
||||
folder: descriptor.path,
|
||||
projectName: descriptor.name ?? projectName ?? existing?.projectName,
|
||||
port: descriptor.port ?? existing?.port ?? 0,
|
||||
pid: descriptor.pid ?? existing?.pid ?? 0,
|
||||
proxyPath: descriptor.proxyPath,
|
||||
|
|
@ -142,8 +143,8 @@ function ensureActiveInstanceSelected(): void {
|
|||
}
|
||||
}
|
||||
|
||||
function upsertWorkspace(descriptor: WorkspaceDescriptor) {
|
||||
const mapped = workspaceDescriptorToInstance(descriptor)
|
||||
function upsertWorkspace(descriptor: WorkspaceDescriptor, projectName?: string) {
|
||||
const mapped = workspaceDescriptorToInstance(descriptor, projectName)
|
||||
if (instances().has(descriptor.id)) {
|
||||
updateInstance(descriptor.id, mapped)
|
||||
} else {
|
||||
|
|
@ -554,10 +555,10 @@ function removeInstance(id: string) {
|
|||
syncHasInstancesFlag()
|
||||
}
|
||||
|
||||
async function createInstance(folder: string, _binaryPath?: string): Promise<string> {
|
||||
async function createInstance(folder: string, _binaryPath?: string, projectName?: string): Promise<string> {
|
||||
try {
|
||||
const workspace = await serverApi.createWorkspace({ path: folder })
|
||||
upsertWorkspace(workspace)
|
||||
const workspace = await serverApi.createWorkspace({ path: folder, name: projectName })
|
||||
upsertWorkspace(workspace, projectName)
|
||||
setActiveInstanceId(workspace.id)
|
||||
return workspace.id
|
||||
} catch (error) {
|
||||
|
|
@ -590,6 +591,18 @@ function getExistingInstanceForFolder(folder: string): Instance | null {
|
|||
return matches.find((instance) => instance.id === activeId) ?? matches.find((instance) => instance.status === "ready") ?? matches[0] ?? null
|
||||
}
|
||||
|
||||
function updateProjectNameForFolder(folder: string, projectName: string): void {
|
||||
const name = projectName.trim()
|
||||
if (!folder || !name) return
|
||||
const target = normalizeInstanceFolderPath(folder)
|
||||
for (const instance of instances().values()) {
|
||||
if (instance.status === "stopped") continue
|
||||
if (normalizeInstanceFolderPath(instance.folder) === target) {
|
||||
updateInstance(instance.id, { projectName: name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function stopInstance(id: string) {
|
||||
const instance = instances().get(id)
|
||||
if (!instance) return
|
||||
|
|
@ -1187,6 +1200,7 @@ export {
|
|||
removeInstance,
|
||||
createInstance,
|
||||
getExistingInstanceForFolder,
|
||||
updateProjectNameForFolder,
|
||||
stopInstance,
|
||||
getActiveInstance,
|
||||
addLog,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export interface OpenCodeBinary {
|
|||
export interface RecentFolder {
|
||||
path: string
|
||||
lastAccessed: number
|
||||
projectName?: string
|
||||
}
|
||||
|
||||
export type ThemePreference = "light" | "dark" | "system"
|
||||
|
|
@ -253,9 +254,14 @@ function normalizeUiState(input?: UiStateBucket | null): NormalizedUiState {
|
|||
if (!f || typeof f !== "object") return null
|
||||
const p = (f as any).path
|
||||
const lastAccessed = (f as any).lastAccessed
|
||||
const projectName = (f as any).projectName
|
||||
if (typeof p !== "string") return null
|
||||
const ts = typeof lastAccessed === "number" ? lastAccessed : Date.now()
|
||||
return { path: p, lastAccessed: ts }
|
||||
return {
|
||||
path: p,
|
||||
lastAccessed: ts,
|
||||
...(typeof projectName === "string" && projectName.trim() ? { projectName: projectName.trim() } : {}),
|
||||
}
|
||||
}),
|
||||
opencodeBinaries: cloneArray<OpenCodeBinary>(source.opencodeBinaries, (b) => {
|
||||
if (!b || typeof b !== "object") return null
|
||||
|
|
@ -335,8 +341,13 @@ function getModelKey(model: { providerId: string; modelId: string }): string {
|
|||
}
|
||||
|
||||
function buildRecentFolderList(folderPath: string, source: RecentFolder[]): RecentFolder[] {
|
||||
const existing = source.find((f) => f.path === folderPath)
|
||||
const folders = source.filter((f) => f.path !== folderPath)
|
||||
folders.unshift({ path: folderPath, lastAccessed: Date.now() })
|
||||
folders.unshift({
|
||||
path: folderPath,
|
||||
lastAccessed: Date.now(),
|
||||
...(existing?.projectName ? { projectName: existing.projectName } : {}),
|
||||
})
|
||||
return folders.slice(0, MAX_RECENT_FOLDERS)
|
||||
}
|
||||
|
||||
|
|
@ -585,6 +596,18 @@ function removeRecentFolder(folderPath: string): void {
|
|||
void patchStateOwner("ui", { recentFolders: next }).catch((error) => log.error("Failed to remove recent folder", error))
|
||||
}
|
||||
|
||||
async function renameRecentFolderProject(folderPath: string, projectName: string): Promise<void> {
|
||||
const name = projectName.trim()
|
||||
if (!folderPath || !name) return
|
||||
const next = recentFolders().map((folder) => (folder.path === folderPath ? { ...folder, projectName: name } : folder))
|
||||
try {
|
||||
await patchStateOwner("ui", { recentFolders: next })
|
||||
} catch (error) {
|
||||
log.error("Failed to rename recent folder", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRemoteServerProfile(input: RemoteServerProfileInput): Promise<RemoteServerProfile> {
|
||||
const profile = buildRemoteServerProfile(input, remoteServers())
|
||||
await patchStateOwner("ui", { remoteServers: buildRemoteServerList(profile, remoteServers()) })
|
||||
|
|
@ -781,6 +804,7 @@ interface ConfigContextValue {
|
|||
uiState: typeof uiState
|
||||
addRecentFolder: typeof addRecentFolder
|
||||
removeRecentFolder: typeof removeRecentFolder
|
||||
renameRecentFolderProject: typeof renameRecentFolderProject
|
||||
addOpenCodeBinary: typeof addOpenCodeBinary
|
||||
removeOpenCodeBinary: typeof removeOpenCodeBinary
|
||||
saveRemoteServerProfile: typeof saveRemoteServerProfile
|
||||
|
|
@ -837,6 +861,7 @@ const configContextValue: ConfigContextValue = {
|
|||
uiState,
|
||||
addRecentFolder,
|
||||
removeRecentFolder,
|
||||
renameRecentFolderProject,
|
||||
addOpenCodeBinary,
|
||||
removeOpenCodeBinary,
|
||||
saveRemoteServerProfile,
|
||||
|
|
@ -924,6 +949,7 @@ export {
|
|||
updateSpeechSettings,
|
||||
addRecentFolder,
|
||||
removeRecentFolder,
|
||||
renameRecentFolderProject,
|
||||
addOpenCodeBinary,
|
||||
removeOpenCodeBinary,
|
||||
recordWorkspaceLaunch,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export interface InstanceMetadata {
|
|||
export interface Instance {
|
||||
id: string
|
||||
folder: string
|
||||
projectName?: string
|
||||
port: number
|
||||
pid: number
|
||||
proxyPath: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue