diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index c0f9e57acc5..5266aa80ee5 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -130,6 +130,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings experimentalTerminalAttention: false, experimentalCompactWorktreeCards: false, experimentalWorktreeSymlinks: false, + experimentalUnifiedNewTabLauncher: false, terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 0e45dfbf399..04cb908443b 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -134,6 +134,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings experimentalTerminalAttention: false, experimentalCompactWorktreeCards: false, experimentalWorktreeSymlinks: false, + experimentalUnifiedNewTabLauncher: false, terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'powershell.exe', enableGitHubAttribution: true, diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx index 7dde24c0ec2..ac003572738 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.tsx @@ -33,6 +33,9 @@ export function ExperimentalPane({ const showWorktreeSymlinks = matchesSettingsSearch(searchQuery, [ EXPERIMENTAL_SEARCH_ENTRY.symlinks ]) + const showUnifiedNewTabLauncher = matchesSettingsSearch(searchQuery, [ + EXPERIMENTAL_SEARCH_ENTRY.unifiedNewTabLauncher + ]) return (
@@ -230,6 +233,46 @@ export function ExperimentalPane({ ) : null} + {showUnifiedNewTabLauncher ? ( + +
+
+ +

+ Type in the New Tab menu to open a terminal, launch an agent, visit a URL, or + open/create a file. +

+
+ +
+
+ ) : null} + {hiddenExperimentalUnlocked ? : null}
) diff --git a/src/renderer/src/components/settings/experimental-search.ts b/src/renderer/src/components/settings/experimental-search.ts index 47f12c6997e..15ad2e7fdc6 100644 --- a/src/renderer/src/components/settings/experimental-search.ts +++ b/src/renderer/src/components/settings/experimental-search.ts @@ -79,6 +79,26 @@ 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' + ] } ] @@ -98,5 +118,6 @@ export const EXPERIMENTAL_SEARCH_ENTRY = { activity: findEntry('Agents View'), terminalAttention: findEntry('Terminal attention'), compactWorktreeCards: findEntry('Compact worktree cards'), - symlinks: findEntry('Symlinks on worktrees') + symlinks: findEntry('Symlinks on worktrees'), + unifiedNewTabLauncher: findEntry('Smart New Tab menu') } as const diff --git a/src/renderer/src/components/tab-bar/TabBar.tsx b/src/renderer/src/components/tab-bar/TabBar.tsx index 13f61e3c584..9437f923cb2 100644 --- a/src/renderer/src/components/tab-bar/TabBar.tsx +++ b/src/renderer/src/components/tab-bar/TabBar.tsx @@ -219,11 +219,17 @@ 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) { @@ -844,7 +850,7 @@ function TabBarInner({ { // Why: terminal-producing menu actions activate a freshly-mounted // xterm. Radix's default focus restore sends focus back to the "+" @@ -853,7 +859,7 @@ function TabBarInner({ runPendingNewTabMenuFocusAfterClose() }} > - {!terminalOnly && onOpenEntry ? ( + {!terminalOnly && onOpenEntry && unifiedNewTabLauncherEnabled ? ( <> unifiedTabsByWorktree: Record activeGroupIdByWorktree: Record - detectedAgentIds: string[] | null - remoteDetectedAgentIds: Record - isDetectingAgents: boolean - isDetectingRemoteAgents: Record } = { activeTabId: null, activeTabType: null, @@ -22,11 +18,7 @@ const appStoreSnapshot: { repos: [], worktreesByRepo: {}, unifiedTabsByWorktree: {}, - activeGroupIdByWorktree: {}, - detectedAgentIds: null, - remoteDetectedAgentIds: {}, - isDetectingAgents: false, - isDetectingRemoteAgents: {} + activeGroupIdByWorktree: {} } const pinTabMock: (tabId: string) => void = vi.fn() const unpinTabMock: (tabId: string) => void = vi.fn() @@ -41,10 +33,6 @@ const useAppStoreMock = vi.fn( worktreesByRepo: Record unifiedTabsByWorktree: Record activeGroupIdByWorktree: Record - detectedAgentIds: string[] | null - remoteDetectedAgentIds: Record - isDetectingAgents: boolean - isDetectingRemoteAgents: Record pinTab: typeof pinTabMock unpinTab: typeof unpinTabMock settings: { @@ -62,10 +50,6 @@ 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: { @@ -123,10 +107,6 @@ 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: { diff --git a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx index 90f07105945..8783fdecc61 100644 --- a/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx +++ b/src/renderer/src/components/tab-bar/TabBarCreateEntry.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' -import { FilePlus, FileText, Globe, Loader2 } from 'lucide-react' +import { FilePlus, FileText, Globe, Loader2, Search } 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 - : 'Open any file, URL, agent, ...' + : 'URL, file, or new file' const submitOption = (option?: ActiveOption) => { if (disabled || pending) { @@ -143,7 +143,7 @@ export default function TabBarCreateEntry({ return (
{ event.preventDefault() submitOption() @@ -164,7 +164,11 @@ export default function TabBarCreateEntry({ }} onPointerDown={(event) => event.stopPropagation()} > -
+
+
{error || activeOptions.length > 0 || hasQuery ? ( -
+
{error ? ( ) : activeOptions.length > 0 ? ( diff --git a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts index 1a68effa620..dcfe9fde228 100644 --- a/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts +++ b/src/renderer/src/components/tab-bar/tab-create-entry-classifier.ts @@ -226,9 +226,7 @@ export function getTabEntryOptions( ): TabEntryOption[] { const trimmed = query.trim() if (!trimmed) { - return [ - { id: 'empty', classification: { kind: 'empty', message: 'Open any file, URL, agent, ...' } } - ] + return [{ id: 'empty', classification: { kind: 'empty', message: 'URL, file, or new file' } }] } const explicitUrl = classifyExplicitUrl(trimmed) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 8cd7a342e50..88ef27732fc 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -306,6 +306,7 @@ 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, diff --git a/src/shared/telemetry-events.ts b/src/shared/telemetry-events.ts index ee378369b64..53f96fd839e 100644 --- a/src/shared/telemetry-events.ts +++ b/src/shared/telemetry-events.ts @@ -278,6 +278,7 @@ export const SETTINGS_CHANGED_WHITELIST = [ 'experimentalActivity', 'experimentalTerminalAttention', 'experimentalWorktreeSymlinks', + 'experimentalUnifiedNewTabLauncher', 'geminiCliOAuthEnabled' ] as const satisfies readonly BooleanGlobalSettingsKey[] export const settingsChangedKeySchema = z.enum(SETTINGS_CHANGED_WHITELIST) diff --git a/src/shared/types.ts b/src/shared/types.ts index 731af475c3f..77cec28a881 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2266,7 +2266,9 @@ 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