Make Smart New Tab launcher the default (remove experimental flag) (#4834) (#4889)

- Remove `experimentalUnifiedNewTabLauncher` flag and gate; the unified
  launcher is now always enabled
- Drop the settings toggle from ExperimentalPane and search index
- Update placeholder/aria text to "Open any file, URL, agent, ..."
- Restyle the input: borderless, flush with dropdown edge, no search icon
- Update test fixtures to include agent-detection state fields now
  unconditionally read by TabBarCreateEntry
This commit is contained in:
Jinjing
2026-06-08 11:05:58 -07:00
committed by GitHub
parent f70a4448ff
commit bcfd84d14c
11 changed files with 36 additions and 94 deletions
@@ -130,7 +130,6 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
experimentalTerminalAttention: false,
experimentalCompactWorktreeCards: false,
experimentalWorktreeSymlinks: false,
experimentalUnifiedNewTabLauncher: false,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'powershell.exe',
enableGitHubAttribution: true,
-1
View File
@@ -134,7 +134,6 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
experimentalTerminalAttention: false,
experimentalCompactWorktreeCards: false,
experimentalWorktreeSymlinks: false,
experimentalUnifiedNewTabLauncher: false,
terminalWindowsShell: 'powershell.exe',
terminalWindowsPowerShellImplementation: 'powershell.exe',
enableGitHubAttribution: true,
@@ -33,9 +33,6 @@ export function ExperimentalPane({
const showWorktreeSymlinks = matchesSettingsSearch(searchQuery, [
EXPERIMENTAL_SEARCH_ENTRY.symlinks
])
const showUnifiedNewTabLauncher = matchesSettingsSearch(searchQuery, [
EXPERIMENTAL_SEARCH_ENTRY.unifiedNewTabLauncher
])
return (
<div className="space-y-4">
@@ -233,46 +230,6 @@ export function ExperimentalPane({
</SearchableSetting>
) : null}
{showUnifiedNewTabLauncher ? (
<SearchableSetting
title="Smart New Tab menu"
description="Type in the New Tab menu to open a terminal, launch an agent, visit a URL, or open/create a file."
keywords={EXPERIMENTAL_SEARCH_ENTRY.unifiedNewTabLauncher.keywords}
className="space-y-3 py-2"
>
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 shrink space-y-0.5">
<Label>Smart New Tab menu</Label>
<p className="text-xs text-muted-foreground">
Type in the New Tab menu to open a terminal, launch an agent, visit a URL, or
open/create a file.
</p>
</div>
<button
type="button"
role="switch"
aria-checked={settings.experimentalUnifiedNewTabLauncher}
onClick={() =>
updateSettings({
experimentalUnifiedNewTabLauncher: !settings.experimentalUnifiedNewTabLauncher
})
}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
settings.experimentalUnifiedNewTabLauncher
? 'bg-foreground'
: 'bg-muted-foreground/30'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-background shadow-sm transition-transform ${
settings.experimentalUnifiedNewTabLauncher ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</button>
</div>
</SearchableSetting>
) : null}
{hiddenExperimentalUnlocked ? <HiddenExperimentalGroup /> : null}
</div>
)
@@ -79,26 +79,6 @@ export const EXPERIMENTAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
'env',
'node_modules'
]
},
{
title: 'Smart New Tab menu',
description:
'Type in the New Tab menu to open a terminal, launch an agent, visit a URL, or open/create a file.',
keywords: [
'experimental',
'smart',
'new tab',
'new tab menu',
'launcher',
'unified',
'plus',
'terminal',
'agents',
'claude',
'codex',
'url',
'file'
]
}
]
@@ -118,6 +98,5 @@ export const EXPERIMENTAL_SEARCH_ENTRY = {
activity: findEntry('Agents View'),
terminalAttention: findEntry('Terminal attention'),
compactWorktreeCards: findEntry('Compact worktree cards'),
symlinks: findEntry('Symlinks on worktrees'),
unifiedNewTabLauncher: findEntry('Smart New Tab menu')
symlinks: findEntry('Symlinks on worktrees')
} as const
@@ -237,17 +237,11 @@ function TabBarInner({
const repo = worktree ? s.repos?.find((entry) => entry.id === worktree.repoId) : null
return Boolean(repo?.connectionId)
})
const unifiedNewTabLauncherEnabled = useAppStore(
(s) => s.settings?.experimentalUnifiedNewTabLauncher === true
)
const defaultAgent = useAppStore((s) => s.settings?.defaultTuiAgent)
const agentCmdOverrides = useAppStore(
(s) => s.settings?.agentCmdOverrides ?? EMPTY_AGENT_CMD_OVERRIDES
)
const connectionId = useAppStore((s) => {
if (!unifiedNewTabLauncherEnabled) {
return undefined
}
const allWorktrees = Object.values(s.worktreesByRepo ?? {}).flat()
const worktree = allWorktrees.find((w) => w.id === worktreeId)
if (!worktree) {
@@ -969,7 +963,7 @@ function TabBarInner({
<DropdownMenuContent
align="start"
sideOffset={6}
className={`${unifiedNewTabLauncherEnabled ? 'w-72 max-w-[calc(100vw-1rem)]' : 'min-w-[11rem]'} rounded-[11px] border-border/80 p-1 shadow-[0_16px_36px_rgba(0,0,0,0.24)]`}
className="w-72 max-w-[calc(100vw-1rem)] rounded-[11px] border-border/80 p-1 shadow-[0_16px_36px_rgba(0,0,0,0.24)]"
onCloseAutoFocus={(e) => {
// Why: terminal-producing menu actions activate a freshly-mounted
// xterm. Radix's default focus restore sends focus back to the "+"
@@ -978,7 +972,7 @@ function TabBarInner({
runPendingNewTabMenuFocusAfterClose()
}}
>
{!terminalOnly && onOpenEntry && unifiedNewTabLauncherEnabled ? (
{!terminalOnly && onOpenEntry ? (
<>
<TabBarCreateEntry
worktreeId={worktreeId}
@@ -11,6 +11,10 @@ const appStoreSnapshot: {
worktreesByRepo: Record<string, { id: string; repoId: string }[]>
unifiedTabsByWorktree: Record<string, unknown[]>
activeGroupIdByWorktree: Record<string, string>
detectedAgentIds: string[] | null
remoteDetectedAgentIds: Record<string, string[]>
isDetectingAgents: boolean
isDetectingRemoteAgents: Record<string, boolean>
} = {
activeTabId: null,
activeTabType: null,
@@ -18,7 +22,11 @@ const appStoreSnapshot: {
repos: [],
worktreesByRepo: {},
unifiedTabsByWorktree: {},
activeGroupIdByWorktree: {}
activeGroupIdByWorktree: {},
detectedAgentIds: null,
remoteDetectedAgentIds: {},
isDetectingAgents: false,
isDetectingRemoteAgents: {}
}
const pinTabMock: (tabId: string) => void = vi.fn()
const unpinTabMock: (tabId: string) => void = vi.fn()
@@ -33,6 +41,10 @@ const useAppStoreMock = vi.fn(
worktreesByRepo: Record<string, { id: string; repoId: string }[]>
unifiedTabsByWorktree: Record<string, unknown[]>
activeGroupIdByWorktree: Record<string, string>
detectedAgentIds: string[] | null
remoteDetectedAgentIds: Record<string, string[]>
isDetectingAgents: boolean
isDetectingRemoteAgents: Record<string, boolean>
pinTab: typeof pinTabMock
unpinTab: typeof unpinTabMock
settings: {
@@ -50,6 +62,10 @@ const useAppStoreMock = vi.fn(
worktreesByRepo: appStoreSnapshot.worktreesByRepo,
unifiedTabsByWorktree: appStoreSnapshot.unifiedTabsByWorktree,
activeGroupIdByWorktree: appStoreSnapshot.activeGroupIdByWorktree,
detectedAgentIds: appStoreSnapshot.detectedAgentIds,
remoteDetectedAgentIds: appStoreSnapshot.remoteDetectedAgentIds,
isDetectingAgents: appStoreSnapshot.isDetectingAgents,
isDetectingRemoteAgents: appStoreSnapshot.isDetectingRemoteAgents,
pinTab: pinTabMock,
unpinTab: unpinTabMock,
settings: {
@@ -107,6 +123,10 @@ useAppStoreExport.getState = vi.fn(() => ({
worktreesByRepo: appStoreSnapshot.worktreesByRepo,
unifiedTabsByWorktree: appStoreSnapshot.unifiedTabsByWorktree,
activeGroupIdByWorktree: appStoreSnapshot.activeGroupIdByWorktree,
detectedAgentIds: appStoreSnapshot.detectedAgentIds,
remoteDetectedAgentIds: appStoreSnapshot.remoteDetectedAgentIds,
isDetectingAgents: appStoreSnapshot.isDetectingAgents,
isDetectingRemoteAgents: appStoreSnapshot.isDetectingRemoteAgents,
pinTab: pinTabMock,
unpinTab: unpinTabMock,
settings: {
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { FilePlus, FileText, Globe, Loader2, Search } from 'lucide-react'
import { FilePlus, FileText, Globe, Loader2 } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { AgentIcon } from '@/lib/agent-catalog'
import { cn } from '@/lib/utils'
@@ -100,7 +100,7 @@ export default function TabBarCreateEntry({
const statusMessage =
statusOption?.classification.kind === 'empty' || statusOption?.classification.kind === 'blocked'
? statusOption.classification.message
: 'URL, file, or new file'
: 'Open any file, URL, agent, ...'
const submitOption = (option?: ActiveOption) => {
if (disabled || pending) {
@@ -143,7 +143,7 @@ export default function TabBarCreateEntry({
return (
<form
className="px-1 pb-1"
className="pb-1"
onSubmit={(event) => {
event.preventDefault()
submitOption()
@@ -164,11 +164,7 @@ export default function TabBarCreateEntry({
}}
onPointerDown={(event) => event.stopPropagation()}
>
<div className="relative">
<Search
className="pointer-events-none absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground"
aria-hidden="true"
/>
<div className="-mx-1 flex items-center border-b border-black/14 px-3 dark:border-white/14">
<Input
ref={inputRef}
value={query}
@@ -177,14 +173,14 @@ export default function TabBarCreateEntry({
setError(null)
}}
disabled={disabled}
aria-label="Open URL, file, or new file"
aria-label="Open any file, URL, agent, ..."
aria-invalid={error ? true : undefined}
placeholder="URL, file, or new file"
className="h-8 rounded-[7px] pl-7 pr-2 text-[12px]"
placeholder="Open any file, URL, agent, ..."
className="h-9 rounded-none border-0 bg-transparent px-0 text-[12px] shadow-none focus-visible:border-0 focus-visible:ring-0 aria-invalid:border-0 aria-invalid:ring-0 dark:bg-transparent"
/>
</div>
{error || activeOptions.length > 0 || hasQuery ? (
<div className="mt-1 space-y-0.5">
<div className="mt-1 space-y-0.5 px-1">
{error ? (
<EntryStatusRow message={error} />
) : activeOptions.length > 0 ? (
@@ -226,7 +226,9 @@ export function getTabEntryOptions(
): TabEntryOption[] {
const trimmed = query.trim()
if (!trimmed) {
return [{ id: 'empty', classification: { kind: 'empty', message: 'URL, file, or new file' } }]
return [
{ id: 'empty', classification: { kind: 'empty', message: 'Open any file, URL, agent, ...' } }
]
}
const explicitUrl = classifyExplicitUrl(trimmed)
-1
View File
@@ -309,7 +309,6 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
experimentalTerminalAttention: false,
experimentalCompactWorktreeCards: false,
experimentalWorktreeSymlinks: false,
experimentalUnifiedNewTabLauncher: false,
// Why: local desktop remains the default server until the user explicitly
// selects a saved runtime environment.
activeRuntimeEnvironmentId: null,
-1
View File
@@ -278,7 +278,6 @@ export const SETTINGS_CHANGED_WHITELIST = [
'experimentalActivity',
'experimentalTerminalAttention',
'experimentalWorktreeSymlinks',
'experimentalUnifiedNewTabLauncher',
'geminiCliOAuthEnabled'
] as const satisfies readonly BooleanGlobalSettingsKey[]
export const settingsChangedKeySchema = z.enum(SETTINGS_CHANGED_WHITELIST)
+1 -3
View File
@@ -2284,9 +2284,7 @@ export type GlobalSettings = {
* configuration surface and edge cases (conflicts with existing paths,
* cleanup on worktree delete) are still being worked out. */
experimentalWorktreeSymlinks: boolean
/** Experimental: replaces the New Tab menu's static preview row with a
* command-style launcher for terminals, detected agents, URLs, and files. */
experimentalUnifiedNewTabLauncher: boolean
/** Active non-local runtime environment for client-routed RPC. `null`
* preserves the current local desktop behavior. */
activeRuntimeEnvironmentId?: string | null