mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-23 16:36:23 +00:00
fix(desktop): separate workspace defaults from live cwd (#69765)
This commit is contained in:
parent
b162371f50
commit
d63a1c4ccb
6 changed files with 23 additions and 136 deletions
|
|
@ -73,7 +73,6 @@ test('creating a branch with ctrl-shift-b updates the composer git-status branch
|
|||
const codingRow = page.locator('.coding-status-bar')
|
||||
const composer = page.locator('[contenteditable="true"]').first()
|
||||
|
||||
await expect(codingRow).toContainText('main')
|
||||
await composer.click()
|
||||
await composer.type('create a repo-backed e2e session', { delay: 2 })
|
||||
await page.keyboard.press('Enter')
|
||||
|
|
@ -82,6 +81,7 @@ test('creating a branch with ctrl-shift-b updates the composer git-status branch
|
|||
'create a repo-backed e2e session',
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
await expect(codingRow).toContainText('main')
|
||||
await page.keyboard.press('Control+Shift+B')
|
||||
|
||||
const branchInput = page.locator('input[placeholder="e.g. my-feature"]').first()
|
||||
|
|
|
|||
|
|
@ -253,10 +253,7 @@ export function ContribWiring({ children }: { children: ReactNode }) {
|
|||
requestGateway
|
||||
})
|
||||
|
||||
const { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds } = useHermesConfig({
|
||||
activeSessionIdRef,
|
||||
refreshProjectBranch
|
||||
})
|
||||
const { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds } = useHermesConfig({ activeSessionIdRef })
|
||||
|
||||
const { refreshCurrentModel, selectModel, updateModelOptionsCache } = useModelControls({
|
||||
queryClient,
|
||||
|
|
|
|||
|
|
@ -47,133 +47,38 @@ describe('useHermesConfig refreshHermesConfig', () => {
|
|||
persistString(WORKSPACE_CWD_KEY, null)
|
||||
})
|
||||
|
||||
it('applies terminal.cwd from config even when localStorage has a stale value', async () => {
|
||||
// Simulate a stale remembered workspace cwd
|
||||
persistString(WORKSPACE_CWD_KEY, '/Users/old/stale-project')
|
||||
setCurrentCwd('/Users/old/stale-project')
|
||||
it('does not let terminal.cwd replace an inactive selected workspace', async () => {
|
||||
setCurrentCwd('/Users/example/repo/.worktrees/feature')
|
||||
|
||||
mockConfig({ terminal: { cwd: '/Users/example/new-workspace' } })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useHermesConfig({ activeSessionIdRef: { current: null } }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
// The configured terminal.cwd must override the stale localStorage value
|
||||
expect($currentCwd.get()).toBe('/Users/example/new-workspace')
|
||||
expect($currentCwd.get()).toBe('/Users/example/repo/.worktrees/feature')
|
||||
})
|
||||
|
||||
it('keeps the active session workspace when a session is running', async () => {
|
||||
setCurrentCwd('/workspace/attached-project')
|
||||
it('does not let terminal.cwd replace an active session workspace', async () => {
|
||||
setCurrentCwd('/Users/example/repo/.worktrees/attached')
|
||||
|
||||
mockConfig({ terminal: { cwd: '/Users/example/new-workspace' } })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: 'session-1' },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useHermesConfig({ activeSessionIdRef: { current: 'session-1' } }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
// Config refreshes mid-session must not yank the workspace out from
|
||||
// under the attached session.
|
||||
expect($currentCwd.get()).toBe('/workspace/attached-project')
|
||||
expect($currentCwd.get()).toBe('/Users/example/repo/.worktrees/attached')
|
||||
})
|
||||
|
||||
it('uses empty string when terminal.cwd is not set and localStorage is empty', async () => {
|
||||
mockConfig({})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
expect($currentCwd.get()).toBe('')
|
||||
})
|
||||
|
||||
it('ignores terminal.cwd when it is "."', async () => {
|
||||
mockConfig({ terminal: { cwd: '.' } })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
expect($currentCwd.get()).toBe('')
|
||||
})
|
||||
|
||||
it('calls refreshProjectBranch with the configured cwd', async () => {
|
||||
const refreshProjectBranch = vi.fn().mockResolvedValue(undefined)
|
||||
setCurrentCwd('')
|
||||
|
||||
mockConfig({ terminal: { cwd: '/workspace/project-a' } })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch
|
||||
})
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
expect(refreshProjectBranch).toHaveBeenCalledWith('/workspace/project-a')
|
||||
})
|
||||
|
||||
it('refreshes the branch for the session cwd (not config) when a session is active', async () => {
|
||||
const refreshProjectBranch = vi.fn().mockResolvedValue(undefined)
|
||||
setCurrentCwd('/workspace/attached-project')
|
||||
|
||||
mockConfig({ terminal: { cwd: '/Users/example/new-workspace' } })
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: 'session-1' },
|
||||
refreshProjectBranch
|
||||
})
|
||||
)
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshHermesConfig()
|
||||
})
|
||||
|
||||
expect(refreshProjectBranch).toHaveBeenCalledWith('/workspace/attached-project')
|
||||
})
|
||||
|
||||
it('does not let a stale forced config refresh overwrite newer draft selector intent', async () => {
|
||||
const profileConfig = deferred<Awaited<ReturnType<typeof getHermesConfig>>>()
|
||||
vi.mocked(getHermesConfig).mockReturnValueOnce(profileConfig.promise)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useHermesConfig({ activeSessionIdRef: { current: null } }))
|
||||
|
||||
let pendingRefresh!: Promise<void>
|
||||
act(() => {
|
||||
|
|
@ -203,12 +108,7 @@ describe('useHermesConfig refreshHermesConfig', () => {
|
|||
const profileC = deferred<Awaited<ReturnType<typeof getHermesConfig>>>()
|
||||
vi.mocked(getHermesConfig).mockReturnValueOnce(profileB.promise).mockReturnValueOnce(profileC.promise)
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useHermesConfig({
|
||||
activeSessionIdRef: { current: null },
|
||||
refreshProjectBranch: vi.fn().mockResolvedValue(undefined)
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useHermesConfig({ activeSessionIdRef: { current: null } }))
|
||||
|
||||
let refreshB!: Promise<void>
|
||||
let refreshC!: Promise<void>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ import { getHermesConfig, getHermesConfigDefaults } from '@/hermes'
|
|||
import { BUILTIN_PERSONALITIES, normalizePersonalityValue, personalityNamesFromConfig } from '@/lib/chat-runtime'
|
||||
import { normalize } from '@/lib/text'
|
||||
import {
|
||||
$currentCwd,
|
||||
getComposerSelectionGeneration,
|
||||
getCurrentModelSource,
|
||||
setAvailablePersonalities,
|
||||
setCurrentCwd,
|
||||
setCurrentFastMode,
|
||||
setCurrentPersonality,
|
||||
setCurrentReasoningEffort,
|
||||
|
|
@ -43,10 +41,9 @@ function normalizeConfigEffort(value: unknown): string {
|
|||
|
||||
interface HermesConfigOptions {
|
||||
activeSessionIdRef: MutableRefObject<string | null>
|
||||
refreshProjectBranch: (cwd: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function useHermesConfig({ activeSessionIdRef, refreshProjectBranch }: HermesConfigOptions) {
|
||||
export function useHermesConfig({ activeSessionIdRef }: HermesConfigOptions) {
|
||||
const [voiceMaxRecordingSeconds, setVoiceMaxRecordingSeconds] = useState(DEFAULT_VOICE_SECONDS)
|
||||
const [sttEnabled, setSttEnabled] = useState(true)
|
||||
const profileRefreshEpochRef = useRef(0)
|
||||
|
|
@ -83,16 +80,6 @@ export function useHermesConfig({ activeSessionIdRef, refreshProjectBranch }: He
|
|||
])
|
||||
])
|
||||
|
||||
const cwd = (config.terminal?.cwd ?? '').trim()
|
||||
|
||||
if (cwd && cwd !== '.') {
|
||||
// Configured terminal.cwd beats a stale remembered workspace cwd
|
||||
// (#38855) — but never yank the workspace out from under an active
|
||||
// session; those keep their own cwd until the user detaches.
|
||||
setCurrentCwd(prev => (activeSessionIdRef.current ? prev : cwd))
|
||||
void refreshProjectBranch($currentCwd.get() || cwd)
|
||||
}
|
||||
|
||||
const reasoning = normalizeConfigEffort(config.agent?.reasoning_effort)
|
||||
const tier = (config.agent?.service_tier ?? '').trim()
|
||||
|
||||
|
|
@ -115,7 +102,7 @@ export function useHermesConfig({ activeSessionIdRef, refreshProjectBranch }: He
|
|||
// Config is nice-to-have; chat still works without it.
|
||||
}
|
||||
},
|
||||
[activeSessionIdRef, refreshProjectBranch]
|
||||
[activeSessionIdRef]
|
||||
)
|
||||
|
||||
return { refreshHermesConfig, sttEnabled, voiceMaxRecordingSeconds }
|
||||
|
|
|
|||
|
|
@ -222,6 +222,15 @@ describe('workspaceCwdForNewSession', () => {
|
|||
expect(workspaceCwdForNewSession()).toBe('/home/user/configured')
|
||||
})
|
||||
|
||||
it('keeps the configured default separate from a selected workspace', () => {
|
||||
setCurrentCwd('/home/user/repo/.worktrees/feature')
|
||||
|
||||
applyConfiguredDefaultProjectDir('/home/user/configured')
|
||||
|
||||
expect(workspaceCwdForNewSession()).toBe('/home/user/configured')
|
||||
expect($currentCwd.get()).toBe('/home/user/repo/.worktrees/feature')
|
||||
})
|
||||
|
||||
it('starts detached (no inherited cwd) when no default project dir is configured', () => {
|
||||
// A bare new chat must NOT inherit the sticky/remembered or live workspace —
|
||||
// that's the "why is my new session already on a branch" bug. Only an
|
||||
|
|
|
|||
|
|
@ -113,12 +113,6 @@ export async function ensureDefaultWorkspaceCwd(): Promise<void> {
|
|||
|
||||
export function applyConfiguredDefaultProjectDir(dir: null | string | undefined): void {
|
||||
configuredDefaultProjectDir = dir?.trim() || ''
|
||||
|
||||
// Cache only — new chats read this via workspaceCwdForNewSession(). Do not
|
||||
// rewrite the live workspace (or localStorage) while a session is active.
|
||||
if (configuredDefaultProjectDir && !$activeSessionId.get()) {
|
||||
setCurrentCwd(configuredDefaultProjectDir)
|
||||
}
|
||||
}
|
||||
|
||||
interface AppAtom<T> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue