From 42943a5451bf973ca2ce36731709ed505ea216f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 15 Jul 2026 11:45:12 +0200 Subject: [PATCH 1/3] feat(ui): make open workspace action explicit Replace the repeated already-open choice dialog with two deterministic actions on recent folders. The highlighted Open control returns directly to the matching instance, while activating the folder row always requests a new instance. Preserve recent-folder aliases when switching, keep native keyboard activation for focused controls, and use accessible project-specific labels plus semantic focus and button tokens across themes. Validated with the UI typecheck, production build, targeted tests, and an independent gatekeeper review. --- packages/ui/src/App.tsx | 72 ++++--------------- .../src/components/folder-selection-view.tsx | 44 +++++++++--- .../lib/i18n/messages/de/folderSelection.ts | 3 - .../lib/i18n/messages/en/folderSelection.ts | 3 - .../lib/i18n/messages/es/folderSelection.ts | 3 - .../lib/i18n/messages/fr/folderSelection.ts | 3 - .../lib/i18n/messages/he/folderSelection.ts | 3 - .../lib/i18n/messages/ja/folderSelection.ts | 3 - .../lib/i18n/messages/ne/folderSelection.ts | 3 - .../lib/i18n/messages/ru/folderSelection.ts | 3 - .../i18n/messages/zh-Hans/folderSelection.ts | 3 - .../ui/src/styles/components/folder-home.css | 33 +++++++++ 12 files changed, 78 insertions(+), 98 deletions(-) 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 @@ -133,6 +134,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 +157,8 @@ const FolderSelectionView: Component = (props) => { return } + if (isInteractiveControl && (e.key === "Enter" || e.key === " ")) return + const listLength = getActiveListLength() if (listLength === 0) return @@ -209,7 +213,7 @@ const FolderSelectionView: Component = (props) => { if (activeTab() === "local") { const folder = folders()[index] if (folder) { - handleFolderSelect(folder.path) + handleFolderSelect(folder.path, true) } return } @@ -309,9 +313,14 @@ 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 resetCloneDialog() { @@ -924,6 +933,9 @@ 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
= (props) => { data-list-index={index()} class="panel-list-item-content flex-1" disabled={isLoading()} - onClick={() => handleFolderSelect(folder.path)} + onClick={() => handleFolderSelect(folder.path, true)} onMouseEnter={() => { if (isLoading()) return setFocusMode("recent") @@ -948,14 +960,9 @@ const FolderSelectionView: Component = (props) => {
- - {getProjectDisplayName(folder.path, folder.projectName)} + + {projectName()} - - - {t("folderSelection.recent.openBadge")} - -
@@ -969,6 +976,21 @@ const FolderSelectionView: Component = (props) => {
+ + {(instance) => ( + + )} + - {(instance) => ( - - )} + {(instance) => { + let ownsHoverState = false + let ownsFocusState = false + onCleanup(() => { + if (ownsHoverState) setRecentActionHovered(folder.path, false) + if (ownsFocusState) setRecentActionFocused(folder.path, false) + }) + return ( + + ) + }} diff --git a/packages/ui/src/styles/components/folder-home.css b/packages/ui/src/styles/components/folder-home.css index 22bef93a0..54d5d4dc2 100644 --- a/packages/ui/src/styles/components/folder-home.css +++ b/packages/ui/src/styles/components/folder-home.css @@ -52,21 +52,23 @@ align-items: center; gap: 0.125rem; padding: 0.375rem 0.625rem; - border: 1px solid var(--button-primary-bg); - border-radius: 0; - background-color: var(--button-primary-bg); - color: var(--button-primary-text); + border: 1px solid var(--border-base); + border-radius: 9999px; + background-color: var(--surface-base); + color: var(--text-secondary); font-size: 0.625rem; font-weight: 700; letter-spacing: 0.05em; line-height: 1; text-transform: uppercase; - transition: background-color 150ms ease, color 150ms ease, box-shadow 150ms ease; + 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 { - background-color: var(--button-primary-hover-bg); + border-color: var(--accent-hover); + background-color: var(--accent-hover); + color: var(--text-on-accent); } .folder-home-open-instance-button:focus-visible { @@ -79,6 +81,16 @@ opacity: 0.5; } +.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; From 0360be0e40a16a23a5b2483dfc2978c88055a21e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Wed, 15 Jul 2026 17:08:55 +0200 Subject: [PATCH 3/3] fix(ui): restore inline open workspace badge Move the Open action back beside the recent workspace name and remove the added chevron, restoring the original compact badge proportions and typography. Keep the full row as an independent accessible new-instance action through a transparent background button, while preserving exclusive hover and focus emphasis for Open, Rename, and Remove. Add a dedicated keyboard focus indicator for the row action. Validated with UI typecheck, production build, diff checks, and an independent gatekeeper pass. --- .../src/components/folder-selection-view.tsx | 110 +++++++++--------- .../ui/src/styles/components/folder-home.css | 27 ++++- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 612fa58e5..7c8652a68 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -960,26 +960,71 @@ const FolderSelectionView: Component = (props) => { "folder-home-recent-item-action-active": hoveredRecentActionPath() === folder.path || focusedRecentActionPath() === folder.path, }} + onMouseEnter={() => { + if (isLoading()) return + setFocusMode("recent") + setSelectedIndex(index()) + }} >
- + ) + }} +
@@ -992,46 +1037,7 @@ const FolderSelectionView: Component = (props) => {
- - - {(instance) => { - let ownsHoverState = false - let ownsFocusState = false - onCleanup(() => { - if (ownsHoverState) setRecentActionHovered(folder.path, false) - if (ownsFocusState) setRecentActionFocused(folder.path, false) - }) - return ( - - ) - }} - +