diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index a0f97a0bb..e5b611aaa 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -35,7 +35,6 @@ import { import { recentFolders, useConfig } from "./stores/preferences" import { createInstance, - getExistingInstanceForFolder, instances, stopInstance, disconnectedInstance, @@ -102,11 +101,6 @@ const App: Component = () => { const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) const [sidecarPickerOpen, setSidecarPickerOpen] = createSignal(false) - const [alreadyOpenFolderChoice, setAlreadyOpenFolderChoice] = createSignal<{ - folderPath: string - binaryPath: string - instanceId: string - } | null>(null) const phoneQuery = useMediaQuery("(max-width: 767px)") const isPhoneLayout = createMemo(() => phoneQuery()) @@ -288,25 +282,17 @@ const App: Component = () => { const projectName = getProjectNameForFolder(folderPath) clearLaunchError() - if (!options?.forceNew) { - const existingInstance = getExistingInstanceForFolder(folderPath) - if (existingInstance) { - recordWorkspaceLaunch(existingInstance.folder, selectedBinary, folderPath) - setAlreadyOpenFolderChoice({ folderPath, binaryPath: selectedBinary, instanceId: existingInstance.id }) - return - } - } - setIsSelectingFolder(true) try { const result = await createInstance(folderPath, selectedBinary, projectName, { forceNew: options?.forceNew }) + recordWorkspaceLaunch(instances().get(result.instanceId)?.folder ?? folderPath, selectedBinary, folderPath) if (result.reused) { - recordWorkspaceLaunch(instances().get(result.instanceId)?.folder ?? folderPath, selectedBinary, folderPath) - setAlreadyOpenFolderChoice({ folderPath, binaryPath: selectedBinary, instanceId: result.instanceId }) + selectInstanceTab(result.instanceId) + setShowFolderSelection(false) + log.info("Selected reused instance", { instanceId: result.instanceId, folderPath }) return } - recordWorkspaceLaunch(instances().get(result.instanceId)?.folder ?? folderPath, selectedBinary, folderPath) selectInstanceTab(result.instanceId) setShowFolderSelection(false) @@ -324,24 +310,13 @@ const App: Component = () => { } } - function dismissAlreadyOpenFolderChoice() { - setAlreadyOpenFolderChoice(null) - } - - function switchToAlreadyOpenFolder() { - const choice = alreadyOpenFolderChoice() - if (!choice) return - setAlreadyOpenFolderChoice(null) - selectInstanceTab(choice.instanceId) + function handleSelectExistingInstance(instanceId: string, recentPath: string, binaryPath: string) { + const instance = instances().get(instanceId) + if (!instance) return + recordWorkspaceLaunch(instance.folder, binaryPath, recentPath) + selectInstanceTab(instanceId) setShowFolderSelection(false) - log.info("Selected existing instance", { instanceId: choice.instanceId, folderPath: choice.folderPath }) - } - - function openAnotherFolderInstance() { - const choice = alreadyOpenFolderChoice() - if (!choice) return - setAlreadyOpenFolderChoice(null) - void handleSelectFolder(choice.folderPath, choice.binaryPath, { forceNew: true }) + log.info("Selected existing instance", { instanceId, folderPath: instance.folder }) } function handleLaunchErrorClose() { @@ -655,6 +630,7 @@ const App: Component = () => { > @@ -665,6 +641,7 @@ const App: Component = () => {
{ @@ -678,31 +655,6 @@ const App: Component = () => { setSidecarPickerOpen(false)} onOpenSidecar={handleOpenSidecar} /> - - !open && dismissAlreadyOpenFolderChoice()}> - - - - - {t("folderSelection.recent.alreadyOpenTitle")} - - - {t("folderSelection.recent.alreadyOpenMessage")} - - -
- - -
-
-
-
-
- void + onSelectExistingInstance: (instanceId: string, recentPath: string, binaryPath: string) => void onOpenSidecar?: () => void isLoading?: boolean onClose?: () => void @@ -50,6 +51,8 @@ const FolderSelectionView: Component = (props) => { } = useConfig() const { t, locale } = useI18n() const [selectedIndex, setSelectedIndex] = createSignal(0) + const [hoveredRecentActionPath, setHoveredRecentActionPath] = createSignal(null) + const [focusedRecentActionPath, setFocusedRecentActionPath] = createSignal(null) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") const [selectedBinary, setSelectedBinary] = createSignal(serverSettings().opencodeBinary || "opencode") const [isFolderBrowserOpen, setIsFolderBrowserOpen] = createSignal(false) @@ -133,6 +136,7 @@ const FolderSelectionView: Component = (props) => { const isEditingField = activeElement && (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || activeElement.isContentEditable || Boolean(insideModal)) + const isInteractiveControl = activeElement && ["BUTTON", "A"].includes(activeElement.tagName) if (isEditingField) { return @@ -155,6 +159,8 @@ const FolderSelectionView: Component = (props) => { return } + if (isInteractiveControl && (e.key === "Enter" || e.key === " ")) return + const listLength = getActiveListLength() if (listLength === 0) return @@ -209,7 +215,7 @@ const FolderSelectionView: Component = (props) => { if (activeTab() === "local") { const folder = folders()[index] if (folder) { - handleFolderSelect(folder.path) + handleFolderSelect(folder.path, true) } return } @@ -309,9 +315,27 @@ const FolderSelectionView: Component = (props) => { return t("time.relative.justNow") } - function handleFolderSelect(path: string) { + function handleFolderSelect(path: string, forceNew = false) { + if (isLoading()) return + props.onSelectFolder(path, selectedBinary(), forceNew ? { forceNew: true } : undefined) + } + + function handleExistingInstanceSelect(instanceId: string, recentPath: string) { if (isLoading()) return - props.onSelectFolder(path, selectedBinary()) + props.onSelectExistingInstance(instanceId, recentPath, selectedBinary()) + } + + function setRecentActionHovered(path: string, active: boolean) { + setHoveredRecentActionPath((current) => active ? path : current === path ? null : current) + } + + function setRecentActionFocused(path: string, active: boolean) { + setFocusedRecentActionPath((current) => active ? path : current === path ? null : current) + } + + function clearRecentActionState(path: string) { + setRecentActionHovered(path, false) + setRecentActionFocused(path, false) } function resetCloneDialog() { @@ -924,37 +948,82 @@ const FolderSelectionView: Component = (props) => { {(folder, index) => { const existingInstance = () => getExistingInstanceForFolder(folder.path) + const projectName = () => getProjectDisplayName(folder.path, folder.projectName) + const projectLabelId = () => `recent-folder-${index()}-name` + const openActionLabelId = () => `recent-folder-${index()}-open-action` return
{ + if (isLoading()) return + setFocusMode("recent") + setSelectedIndex(index()) }} >
- + ) + }}
@@ -968,20 +1037,31 @@ const FolderSelectionView: Component = (props) => {
- +
diff --git a/packages/ui/src/lib/i18n/messages/de/folderSelection.ts b/packages/ui/src/lib/i18n/messages/de/folderSelection.ts index 55d99264b..a79c23311 100644 --- a/packages/ui/src/lib/i18n/messages/de/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/de/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "Arbeitsbereich umbenennen", "folderSelection.recent.remove": "Aus Liste entfernen", "folderSelection.recent.openBadge": "Offen", - "folderSelection.recent.alreadyOpenTitle": "Projekt bereits offen", - "folderSelection.recent.alreadyOpenMessage": "Wählen Sie, wie dieser Ordner geöffnet werden soll.", "folderSelection.recent.switchToOpenProject": "Zum offenen Projekt wechseln", - "folderSelection.recent.openAnotherInstance": "Weitere Instanz öffnen", "folderSelection.browse.title": "Nach Ordner suchen", "folderSelection.browse.subtitle": "Wählen Sie einen beliebigen Ordner auf Ihrem Computer aus", diff --git a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts index 9ff397715..6d20df29b 100644 --- a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "Rename workspace", "folderSelection.recent.remove": "Remove from recent", "folderSelection.recent.openBadge": "Open", - "folderSelection.recent.alreadyOpenTitle": "Project already open", - "folderSelection.recent.alreadyOpenMessage": "Choose how to open this folder.", "folderSelection.recent.switchToOpenProject": "Switch to open project", - "folderSelection.recent.openAnotherInstance": "Open another instance", "folderSelection.browse.title": "Browse for Folder", "folderSelection.browse.subtitle": "Select any folder on your computer", diff --git a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts index e6ad48724..be7bdc0be 100644 --- a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "Renombrar workspace", "folderSelection.recent.remove": "Eliminar de recientes", "folderSelection.recent.openBadge": "Abierta", - "folderSelection.recent.alreadyOpenTitle": "Proyecto ya abierto", - "folderSelection.recent.alreadyOpenMessage": "Elige cómo abrir esta carpeta.", "folderSelection.recent.switchToOpenProject": "Cambiar al proyecto abierto", - "folderSelection.recent.openAnotherInstance": "Abrir otra instancia", "folderSelection.browse.title": "Buscar carpeta", "folderSelection.browse.subtitle": "Selecciona cualquier carpeta en tu ordenador", diff --git a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts index dd6de4865..c9aef074e 100644 --- a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "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", - "folderSelection.recent.alreadyOpenMessage": "Choisissez comment ouvrir ce dossier.", "folderSelection.recent.switchToOpenProject": "Basculer vers le projet ouvert", - "folderSelection.recent.openAnotherInstance": "Ouvrir une autre instance", "folderSelection.browse.title": "Parcourir un dossier", "folderSelection.browse.subtitle": "Sélectionnez n'importe quel dossier sur votre ordinateur", diff --git a/packages/ui/src/lib/i18n/messages/he/folderSelection.ts b/packages/ui/src/lib/i18n/messages/he/folderSelection.ts index e317ffe05..63e1a57b8 100644 --- a/packages/ui/src/lib/i18n/messages/he/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/he/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "שנה שם סביבת עבודה", "folderSelection.recent.remove": "הסר מהרשימה האחרונה", "folderSelection.recent.openBadge": "פתוח", - "folderSelection.recent.alreadyOpenTitle": "הפרויקט כבר פתוח", - "folderSelection.recent.alreadyOpenMessage": "בחר כיצד לפתוח את התיקייה הזו.", "folderSelection.recent.switchToOpenProject": "עבור לפרויקט הפתוח", - "folderSelection.recent.openAnotherInstance": "פתח מופע נוסף", "folderSelection.browse.title": "עיון בתיקייה", "folderSelection.browse.subtitle": "בחר כל תיקייה במחשב שלך", diff --git a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts index 0cf6b4ea0..0f4e2a6c7 100644 --- a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "ワークスペース名を変更", "folderSelection.recent.remove": "履歴から削除", "folderSelection.recent.openBadge": "開いています", - "folderSelection.recent.alreadyOpenTitle": "プロジェクトはすでに開いています", - "folderSelection.recent.alreadyOpenMessage": "このフォルダの開き方を選択してください。", "folderSelection.recent.switchToOpenProject": "開いているプロジェクトに切り替え", - "folderSelection.recent.openAnotherInstance": "別のインスタンスを開く", "folderSelection.browse.title": "フォルダを参照", "folderSelection.browse.subtitle": "コンピュータ上の任意のフォルダを選択", diff --git a/packages/ui/src/lib/i18n/messages/ne/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ne/folderSelection.ts index 6cf0e4813..020e550dd 100644 --- a/packages/ui/src/lib/i18n/messages/ne/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ne/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "कार्यस्थान पुन: नामकरण गर्नुहोस्", "folderSelection.recent.remove": "भर्खरको सूचीबाट हटाउनुहोस्", "folderSelection.recent.openBadge": "खोल्नुहोस्", - "folderSelection.recent.alreadyOpenTitle": "परियोजना पहिले नै खुला छ", - "folderSelection.recent.alreadyOpenMessage": "यो फोल्डर कसरी खोल्ने छनौट गर्नुहोस्।", "folderSelection.recent.switchToOpenProject": "खुला परियोजनामा जानुहोस्", - "folderSelection.recent.openAnotherInstance": "अर्को उदाहरण खोल्नुहोस्", "folderSelection.browse.title": "फोल्डरको लागि ब्राउज गर्नुहोस्", "folderSelection.browse.subtitle": "तपाईंको कम्प्युटरमा कुनै पनि फोल्डर चयन गर्नुहोस्", diff --git a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts index 3229bd2f1..dddcbd9db 100644 --- a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "Переименовать рабочее пространство", "folderSelection.recent.remove": "Убрать из недавних", "folderSelection.recent.openBadge": "Открыта", - "folderSelection.recent.alreadyOpenTitle": "Проект уже открыт", - "folderSelection.recent.alreadyOpenMessage": "Выберите, как открыть эту папку.", "folderSelection.recent.switchToOpenProject": "Перейти к открытому проекту", - "folderSelection.recent.openAnotherInstance": "Открыть еще одну инстанцию", "folderSelection.browse.title": "Выбрать папку", "folderSelection.browse.subtitle": "Выберите любую папку на компьютере", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts index 21b2410e7..1b789a2a9 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts @@ -17,10 +17,7 @@ export const folderSelectionMessages = { "folderSelection.recent.rename": "重命名工作区", "folderSelection.recent.remove": "从最近列表移除", "folderSelection.recent.openBadge": "已打开", - "folderSelection.recent.alreadyOpenTitle": "项目已打开", - "folderSelection.recent.alreadyOpenMessage": "选择如何打开此文件夹。", "folderSelection.recent.switchToOpenProject": "切换到已打开的项目", - "folderSelection.recent.openAnotherInstance": "打开另一个实例", "folderSelection.browse.title": "浏览文件夹", "folderSelection.browse.subtitle": "选择你电脑上的任意文件夹", diff --git a/packages/ui/src/styles/components/folder-home.css b/packages/ui/src/styles/components/folder-home.css index a62fcbe23..7e1256e61 100644 --- a/packages/ui/src/styles/components/folder-home.css +++ b/packages/ui/src/styles/components/folder-home.css @@ -46,6 +46,72 @@ max-height: var(--folder-home-actions-height, none); } +.folder-home-open-instance-button { + display: inline-flex; + flex-shrink: 0; + align-items: center; + padding: 0.125rem 0.5rem; + border: 1px solid var(--border-base); + border-radius: 9999px; + background-color: var(--surface-base); + color: var(--text-secondary); + font-size: 0.625rem; + font-weight: 500; + letter-spacing: 0.05em; + line-height: 1; + text-transform: uppercase; + transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, box-shadow 150ms ease; +} + +.folder-home-open-instance-button:hover:not(:disabled), +.folder-home-open-instance-button:focus-visible { + border-color: var(--accent-hover); + background-color: var(--accent-hover); + color: var(--text-on-accent); +} + +.folder-home-open-instance-button:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color); +} + +.folder-home-open-instance-button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.folder-home-recent-primary-action { + position: absolute; + inset: 0; + z-index: 0; + width: 100%; + border: 0; + background: transparent; + cursor: pointer; +} + +.folder-home-recent-primary-action:focus { + outline: none; +} + +.folder-home-recent-primary-action:focus-visible { + box-shadow: inset 0 0 0 2px var(--focus-ring-color); +} + +.folder-home-recent-primary-action:disabled { + cursor: not-allowed; +} + +.folder-home-row-action:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--focus-ring-offset), 0 0 0 4px var(--focus-ring-color); +} + +.folder-home-recent-item.folder-home-recent-item-action-active { + background-color: transparent !important; + box-shadow: none !important; +} + @media (min-width: 1024px) { .folder-home-main { display: grid;