Gate Agents view behind Experimental (#2182)
This commit is contained in:
parent
178e885019
commit
20389bf02d
|
|
@ -222,7 +222,8 @@ describe('Store', () => {
|
|||
expect(settings.showTasksButton).toBe(true)
|
||||
expect(settings.visibleTaskProviders).toEqual(['github', 'gitlab', 'linear'])
|
||||
expect(settings.openInApplications).toEqual([])
|
||||
expect(settings.experimentalActivity).toBe(true)
|
||||
expect(settings.experimentalActivity).toBe(false)
|
||||
expect(settings.experimentalActivityDefaultedOffForAllUsers).toBe(true)
|
||||
expect(settings.floatingTerminalEnabled).toBe(true)
|
||||
expect(settings.floatingTerminalDefaultedForAllUsers).toBe(true)
|
||||
expect(settings.notifications.customSoundPath).toBeNull()
|
||||
|
|
@ -615,7 +616,8 @@ describe('Store', () => {
|
|||
expect(store.getSettings().showTasksButton).toBe(true)
|
||||
expect(store.getSettings().combinedDiffFileTreeVisibleByDefault).toBe(false)
|
||||
expect(store.getSettings().visibleTaskProviders).toEqual(['github', 'gitlab', 'linear'])
|
||||
expect(store.getSettings().experimentalActivity).toBe(true)
|
||||
expect(store.getSettings().experimentalActivity).toBe(false)
|
||||
expect(store.getSettings().experimentalActivityDefaultedOffForAllUsers).toBe(true)
|
||||
expect(store.getSettings().notifications.customSoundPath).toBeNull()
|
||||
// repos should be loaded
|
||||
expect(store.getRepos()).toHaveLength(1)
|
||||
|
|
@ -1457,12 +1459,32 @@ describe('Store', () => {
|
|||
expect(store.getSettings().experimentalPet).toBe(true)
|
||||
})
|
||||
|
||||
it('promotes legacy experimentalActivity profiles to default-on', async () => {
|
||||
it('defaults legacy experimentalActivity profiles off once', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: { experimentalActivity: false },
|
||||
settings: { experimentalActivity: true },
|
||||
ui: {},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
})
|
||||
|
||||
const store = await createStore()
|
||||
|
||||
expect(store.getSettings().experimentalActivity).toBe(false)
|
||||
expect(store.getSettings().experimentalActivityDefaultedOffForAllUsers).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves experimentalActivity after the default-off migration has run', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: {
|
||||
experimentalActivity: true,
|
||||
experimentalActivityDefaultedOffForAllUsers: true
|
||||
},
|
||||
ui: {},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
|
|
|
|||
|
|
@ -1210,6 +1210,13 @@ export class Store {
|
|||
const migratedFloatingTerminalEnabled = floatingTerminalDefaultedForAllUsers
|
||||
? (parsed.settings?.floatingTerminalEnabled ?? true)
|
||||
: true
|
||||
const experimentalActivityDefaultedOffForAllUsers =
|
||||
parsed.settings?.experimentalActivityDefaultedOffForAllUsers === true
|
||||
// Why: the Agents view moved back behind Experimental. Flip every
|
||||
// pre-migration profile off once, then preserve future user opt-ins.
|
||||
const migratedExperimentalActivity = experimentalActivityDefaultedOffForAllUsers
|
||||
? (parsed.settings?.experimentalActivity ?? false)
|
||||
: false
|
||||
result = {
|
||||
...defaults,
|
||||
...parsed,
|
||||
|
|
@ -1221,10 +1228,8 @@ export class Store {
|
|||
// the old persisted flag forward once so enabled users don't lose it.
|
||||
experimentalPet:
|
||||
parsed.settings?.experimentalPet ?? readLegacySidekickFlag(parsed) ?? false,
|
||||
// Why: Activity graduated from its experimental gate. Force the
|
||||
// legacy flag on so existing profiles and rollback builds see the
|
||||
// same default-on behavior as fresh installs.
|
||||
experimentalActivity: true,
|
||||
experimentalActivity: migratedExperimentalActivity,
|
||||
experimentalActivityDefaultedOffForAllUsers: true,
|
||||
terminalMacOptionAsAlt: migratedOptionAsAlt,
|
||||
terminalMacOptionAsAltMigrated: true,
|
||||
floatingTerminalEnabled: migratedFloatingTerminalEnabled,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function ExperimentalPane({
|
|||
}: ExperimentalPaneProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((s) => s.settingsSearchQuery)
|
||||
const showPet = matchesSettingsSearch(searchQuery, [EXPERIMENTAL_SEARCH_ENTRY.pet])
|
||||
const showActivity = matchesSettingsSearch(searchQuery, [EXPERIMENTAL_SEARCH_ENTRY.activity])
|
||||
const showAgentsView = matchesSettingsSearch(searchQuery, [EXPERIMENTAL_SEARCH_ENTRY.activity])
|
||||
const showWorktreeSymlinks = matchesSettingsSearch(searchQuery, [
|
||||
EXPERIMENTAL_SEARCH_ENTRY.symlinks
|
||||
])
|
||||
|
|
@ -69,18 +69,18 @@ export function ExperimentalPane({
|
|||
</SearchableSetting>
|
||||
) : null}
|
||||
|
||||
{showActivity ? (
|
||||
{showAgentsView ? (
|
||||
<SearchableSetting
|
||||
title="Activity Page"
|
||||
description="Slack-style worktree activity feed for agent completions and blocking states."
|
||||
title="Agents View"
|
||||
description="Threaded left-sidebar feed for agent completions and blocking states."
|
||||
keywords={EXPERIMENTAL_SEARCH_ENTRY.activity.keywords}
|
||||
className="space-y-3 px-1 py-2"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 shrink space-y-0.5">
|
||||
<Label>Activity Page</Label>
|
||||
<Label>Agents View</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Adds an Activity entry under Tasks with a threaded worktree feed for completed
|
||||
Adds an Agents entry to the left sidebar with a threaded worktree feed for completed
|
||||
agents, blocking questions, unread state, and worktree creation events. Experimental
|
||||
— the event model and UI may change.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -16,17 +16,19 @@ export const EXPERIMENTAL_PANE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
]
|
||||
},
|
||||
{
|
||||
title: 'Activity Page',
|
||||
description: 'Slack-style worktree activity feed for agent completions and blocking states.',
|
||||
title: 'Agents View',
|
||||
description: 'Threaded left-sidebar feed for agent completions and blocking states.',
|
||||
keywords: [
|
||||
'experimental',
|
||||
'agents',
|
||||
'agents view',
|
||||
'activity',
|
||||
'notifications',
|
||||
'agents',
|
||||
'worktrees',
|
||||
'timeline',
|
||||
'unread',
|
||||
'bell'
|
||||
'bell',
|
||||
'sidebar'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -61,6 +63,6 @@ function findEntry(title: string): SettingsSearchEntry {
|
|||
|
||||
export const EXPERIMENTAL_SEARCH_ENTRY = {
|
||||
pet: findEntry('Pet'),
|
||||
activity: findEntry('Activity Page'),
|
||||
activity: findEntry('Agents View'),
|
||||
symlinks: findEntry('Symlinks on worktrees')
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../../shared/constants'
|
||||
import { shouldShowAgentsButton } from './SidebarNav'
|
||||
|
||||
describe('SidebarNav', () => {
|
||||
it('hides the Agents entry while settings are loading', () => {
|
||||
expect(shouldShowAgentsButton(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the Agents entry while the experimental Agents view is off', () => {
|
||||
expect(
|
||||
shouldShowAgentsButton({
|
||||
...getDefaultSettings('/tmp'),
|
||||
experimentalActivity: false
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the Agents entry when the experimental Agents view is on', () => {
|
||||
expect(
|
||||
shouldShowAgentsButton({
|
||||
...getDefaultSettings('/tmp'),
|
||||
experimentalActivity: true
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -4,6 +4,7 @@ import { useAppStore } from '@/store'
|
|||
import { useRepoMap } from '@/store/selectors'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { isGitRepoKind } from '../../../../shared/repo-kind'
|
||||
import type { GlobalSettings } from '../../../../shared/types'
|
||||
import { getTaskPresetQuery, PER_REPO_FETCH_LIMIT } from '@/lib/new-workspace'
|
||||
import { LinearIcon } from '@/components/icons/LinearIcon'
|
||||
import { migrationUnsupportedToAgentStatusEntry } from '@/lib/migration-unsupported-agent-entry'
|
||||
|
|
@ -18,6 +19,12 @@ const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('
|
|||
// rows under that strip should remain clickable when their bounds overlap.
|
||||
const SIDEBAR_NAV_HIT_TARGET_CLASS = 'relative z-20'
|
||||
|
||||
export function shouldShowAgentsButton(
|
||||
settings: Pick<GlobalSettings, 'experimentalActivity'> | null | undefined
|
||||
): boolean {
|
||||
return settings?.experimentalActivity === true
|
||||
}
|
||||
|
||||
const SidebarNav = React.memo(function SidebarNav() {
|
||||
const openTaskPage = useAppStore((s) => s.openTaskPage)
|
||||
const openAutomationsPage = useAppStore((s) => s.openAutomationsPage)
|
||||
|
|
@ -38,6 +45,7 @@ const SidebarNav = React.memo(function SidebarNav() {
|
|||
const linearStatus = useAppStore((s) => s.linearStatus)
|
||||
const linearStatusChecked = useAppStore((s) => s.linearStatusChecked)
|
||||
const checkLinearConnection = useAppStore((s) => s.checkLinearConnection)
|
||||
const showAgentsButton = useAppStore((s) => shouldShowAgentsButton(s.settings))
|
||||
const preferredVisibleTaskProviders = React.useMemo(
|
||||
() => normalizeVisibleTaskProviders(rawVisibleTaskProviders),
|
||||
[rawVisibleTaskProviders]
|
||||
|
|
@ -241,29 +249,31 @@ const SidebarNav = React.memo(function SidebarNav() {
|
|||
/>
|
||||
<span className="flex-1">Automations</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openActivityPage}
|
||||
aria-current={activityActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
SIDEBAR_NAV_HIT_TARGET_CLASS,
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
|
||||
activityActive
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground/60 hover:bg-sidebar-foreground/8'
|
||||
)}
|
||||
>
|
||||
<Bell
|
||||
className={cn('size-4 shrink-0', !activityActive && 'text-sidebar-foreground/30')}
|
||||
strokeWidth={activityActive ? 2.25 : 1.75}
|
||||
/>
|
||||
<span className="flex-1">Agents</span>
|
||||
{activityUnreadCount > 0 ? (
|
||||
<span className="rounded-full bg-primary px-1.5 py-px text-[10px] font-semibold text-primary-foreground">
|
||||
{activityUnreadCount}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{showAgentsButton ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openActivityPage}
|
||||
aria-current={activityActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
SIDEBAR_NAV_HIT_TARGET_CLASS,
|
||||
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] font-medium tracking-tight transition-colors',
|
||||
activityActive
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground/60 hover:bg-sidebar-foreground/8'
|
||||
)}
|
||||
>
|
||||
<Bell
|
||||
className={cn('size-4 shrink-0', !activityActive && 'text-sidebar-foreground/30')}
|
||||
strokeWidth={activityActive ? 2.25 : 1.75}
|
||||
/>
|
||||
<span className="flex-1">Agents</span>
|
||||
{activityUnreadCount > 0 ? (
|
||||
<span className="rounded-full bg-primary px-1.5 py-px text-[10px] font-semibold text-primary-foreground">
|
||||
{activityUnreadCount}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal('worktree-palette')}
|
||||
|
|
|
|||
|
|
@ -605,12 +605,16 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
worktreeNavHistoryIndex: nextHistoryIndex
|
||||
}
|
||||
}),
|
||||
openActivityPage: () =>
|
||||
openActivityPage: () => {
|
||||
if (get().settings?.experimentalActivity !== true) {
|
||||
return
|
||||
}
|
||||
set((state) => ({
|
||||
activeView: 'activity',
|
||||
previousViewBeforeActivity:
|
||||
state.activeView === 'activity' ? state.previousViewBeforeActivity : state.activeView
|
||||
})),
|
||||
}))
|
||||
},
|
||||
closeActivityPage: () =>
|
||||
set((state) => ({
|
||||
activeView: state.previousViewBeforeActivity
|
||||
|
|
@ -660,9 +664,14 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
state.activeView === 'settings' ? state.previousViewBeforeSettings : state.activeView
|
||||
})),
|
||||
closeSettingsPage: () =>
|
||||
set((state) => ({
|
||||
activeView: state.previousViewBeforeSettings
|
||||
})),
|
||||
set((state) => {
|
||||
const previousView =
|
||||
state.previousViewBeforeSettings === 'activity' &&
|
||||
state.settings?.experimentalActivity !== true
|
||||
? 'terminal'
|
||||
: state.previousViewBeforeSettings
|
||||
return { activeView: previousView }
|
||||
}),
|
||||
settingsNavigationTarget: null,
|
||||
openSettingsTarget: (target) => set({ settingsNavigationTarget: target }),
|
||||
clearSettingsTarget: () => set({ settingsNavigationTarget: null }),
|
||||
|
|
|
|||
|
|
@ -259,7 +259,8 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
// Why: off by default — opt-in cosmetic joke feature. Leaving the default
|
||||
// false keeps the overlay unmounted for users who never enable it.
|
||||
experimentalPet: false,
|
||||
experimentalActivity: true,
|
||||
experimentalActivity: false,
|
||||
experimentalActivityDefaultedOffForAllUsers: true,
|
||||
experimentalWorktreeSymlinks: false,
|
||||
// Why: local desktop remains the default server until the user explicitly
|
||||
// selects a saved runtime environment.
|
||||
|
|
|
|||
|
|
@ -197,6 +197,7 @@ export const SETTINGS_CHANGED_WHITELIST = [
|
|||
'openLinksInApp',
|
||||
'experimentalMobile',
|
||||
'experimentalPet',
|
||||
'experimentalActivity',
|
||||
'experimentalWorktreeSymlinks',
|
||||
'geminiCliOAuthEnabled'
|
||||
] as const satisfies readonly BooleanGlobalSettingsKey[]
|
||||
|
|
|
|||
|
|
@ -1579,9 +1579,12 @@ export type GlobalSettings = {
|
|||
/** Legacy persisted key from before the sidekick -> pet rename. Read only
|
||||
* during migration; new writes use experimentalPet. */
|
||||
experimentalSidekick?: boolean
|
||||
/** Legacy persisted flag from when Activity was experimental. Activity is
|
||||
* now default-on and this no longer gates the page. */
|
||||
/** Experimental: left-sidebar Agents view with a threaded feed for agent
|
||||
* completions, blocking states, unread state, and worktree creation events. */
|
||||
experimentalActivity: boolean
|
||||
/** One-shot migration guard for defaulting the Agents view off for all
|
||||
* users. Once set, later explicit opt-ins persist normally. */
|
||||
experimentalActivityDefaultedOffForAllUsers?: boolean
|
||||
/** Experimental: when creating a worktree, automatically symlink a
|
||||
* user-configured set of files/folders from the primary checkout (e.g.
|
||||
* `.env`, `node_modules`) into the new worktree. Opt-in while the
|
||||
|
|
|
|||
Loading…
Reference in New Issue