From a5c0715835250a49aacf8f7858dcf5f375f52ead Mon Sep 17 00:00:00 2001 From: harjoth Date: Sun, 12 Jul 2026 04:43:43 -0400 Subject: [PATCH] fix(desktop): preserve sidebar workspace targets across new drafts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed salvage of #45744 (@harjothkhara), rebased onto current main and resolved against #58241 (which swapped the new-session cwd fallback to the project-aware resolveNewSessionCwd). An explicitly clicked sidebar workspace stays authoritative until session.create: a one-shot $newChatWorkspaceTarget (null → detached, string → that folder) plus a generation counter so a stale async `config.get project` normalization can't overwrite a newer draft target. The start-workspace-session action is extracted out of desktop-controller.tsx into a testable workspace-session-target module. Integrated with #58241: the no-explicit-target branch now falls through to the project-aware resolveNewSessionCwd() instead of the old workspaceCwdForNewSession. Co-authored-by: harjoth --- apps/desktop/src/app/desktop-controller.tsx | 49 ++------- .../session/hooks/use-cwd-actions.test.tsx | 102 ++++++++++++++++++ .../src/app/session/hooks/use-cwd-actions.ts | 20 +++- .../hooks/use-session-actions.test.tsx | 79 ++++++++++++-- .../hooks/use-session-actions/index.ts | 55 ++++++++-- .../session/workspace-session-target.test.ts | 81 ++++++++++++++ .../app/session/workspace-session-target.ts | 64 +++++++++++ apps/desktop/src/store/session.ts | 13 +++ 8 files changed, 406 insertions(+), 57 deletions(-) create mode 100644 apps/desktop/src/app/session/hooks/use-cwd-actions.test.tsx create mode 100644 apps/desktop/src/app/session/workspace-session-target.test.ts create mode 100644 apps/desktop/src/app/session/workspace-session-target.ts diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index 2ff2ff176e73..033cae9b5fa8 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -47,7 +47,7 @@ import { } from '../store/pet-overlay' import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview' import { $activeGatewayProfile, $freshSessionRequest, $profileScope, refreshActiveProfile } from '../store/profile' -import { $startWorkSessionRequest, followActiveSessionCwd, resolveNewSessionCwd } from '../store/projects' +import { $startWorkSessionRequest, followActiveSessionCwd } from '../store/projects' import { $reviewOpen, REVIEW_PANE_ID } from '../store/review' import { $activeSessionId, @@ -65,8 +65,6 @@ import { sessionPinId, setAwaitingResponse, setBusy, - setCurrentBranch, - setCurrentCwd, setCurrentModel, setCurrentProvider, setMessages, @@ -117,6 +115,7 @@ import { useRouteResume } from './session/hooks/use-route-resume' import { useSessionActions } from './session/hooks/use-session-actions' import { useSessionListActions } from './session/hooks/use-session-list-actions' import { useSessionStateCache } from './session/hooks/use-session-state-cache' +import { startWorkspaceSession } from './session/workspace-session-target' import { AppShell } from './shell/app-shell' import { useOverlayRouting } from './shell/hooks/use-overlay-routing' import { useStatusSnapshot } from './shell/hooks/use-status-snapshot' @@ -735,42 +734,16 @@ export function DesktopController() { const startSessionInWorkspace = useCallback( (path: null | string) => { - startFreshSessionDraft() - - // A worktree lane carries its own path; the trunk "+" can be path-less (the - // main checkout is implicit), so fall back to the active project's root - // instead of no-op'ing on null — that was "+ on main does nothing". - const target = path?.trim() || resolveNewSessionCwd() - - if (!target) { - return - } - - // The next message creates the backend session in $currentCwd, so seed - // it (and the branch) from the workspace the user clicked the + on. - setCurrentCwd(target) - void requestGateway<{ branch?: string; cwd?: string }>('config.get', { key: 'project', cwd: target }) - .then(info => { - const resolved = info.cwd || target - - setCurrentCwd(resolved) - setCurrentBranch(info.branch || '') - - // An EXPLICIT target (a worktree/lane path — e.g. just-created via - // "convert a branch" / "new worktree") drills the sidebar into that - // project so the new lane is visible at once. Without this, a brand-new - // worktree session is invisible from the all-projects overview (the - // live overlay skips `.worktrees` rows, and the session.info cwd-follow - // only fires on a same-session move, not a fresh session). The - // path-less trunk "+" keeps the current scope untouched. - if (path?.trim()) { - restoreWorktree(resolved) - void followActiveSessionCwd(resolved) - } - }) - .catch(() => undefined) + startWorkspaceSession({ + activeSessionIdRef, + followActiveSessionCwd, + onExplicitWorkspace: restoreWorktree, + path, + requestGateway, + startFreshSessionDraft + }) }, - [requestGateway, startFreshSessionDraft] + [activeSessionIdRef, requestGateway, startFreshSessionDraft] ) // Composer "branch off into a new worktree": the composer already created the diff --git a/apps/desktop/src/app/session/hooks/use-cwd-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-cwd-actions.test.tsx new file mode 100644 index 000000000000..aba64c51738d --- /dev/null +++ b/apps/desktop/src/app/session/hooks/use-cwd-actions.test.tsx @@ -0,0 +1,102 @@ +import { act, cleanup, render, waitFor } from '@testing-library/react' +import type { MutableRefObject } from 'react' +import { useEffect } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { + $currentBranch, + $currentCwd, + $newChatWorkspaceTarget, + setCurrentBranch, + setCurrentCwd, + setCurrentCwdTransient, + setNewChatWorkspaceTarget +} from '@/store/session' + +import { useCwdActions } from './use-cwd-actions' + +type CwdActionsHandle = ReturnType + +function deferred() { + let resolve!: (value: T) => void + + const promise = new Promise(done => { + resolve = done + }) + + return { promise, resolve } +} + +function Harness({ + activeSessionIdRef, + onReady, + requestGateway +}: { + activeSessionIdRef: MutableRefObject + onReady: (handle: CwdActionsHandle) => void + requestGateway: (method: string, params?: Record) => Promise +}) { + const actions = useCwdActions({ + activeSessionId: activeSessionIdRef.current, + activeSessionIdRef, + requestGateway + }) + + useEffect(() => { + onReady(actions) + }, [actions, onReady]) + + return null +} + +describe('useCwdActions draft workspace target', () => { + beforeEach(() => { + setCurrentCwd('') + setCurrentBranch('') + setNewChatWorkspaceTarget(undefined) + }) + + afterEach(() => { + cleanup() + setCurrentCwd('') + setCurrentBranch('') + setNewChatWorkspaceTarget(undefined) + vi.restoreAllMocks() + }) + + it('ignores stale draft cwd normalization after a newer no-workspace target wins', async () => { + const projectInfo = deferred<{ branch?: string; cwd?: string }>() + const requestGateway = vi.fn(async () => projectInfo.promise as never) + const activeSessionIdRef: MutableRefObject = { current: null } + let handle: CwdActionsHandle | null = null + + render( + (handle = h)} + requestGateway={requestGateway} + /> + ) + await waitFor(() => expect(handle).not.toBeNull()) + + let pendingChange!: Promise + + await act(async () => { + pendingChange = handle!.changeSessionCwd('/stale-workspace') + }) + + expect($newChatWorkspaceTarget.get()).toBe('/stale-workspace') + + setNewChatWorkspaceTarget(null) + setCurrentCwdTransient('') + projectInfo.resolve({ branch: 'main', cwd: '/normalized-stale-workspace' }) + + await act(async () => { + await pendingChange + }) + + expect($newChatWorkspaceTarget.get()).toBeNull() + expect($currentCwd.get()).toBe('') + expect($currentBranch.get()).toBe('') + }) +}) diff --git a/apps/desktop/src/app/session/hooks/use-cwd-actions.ts b/apps/desktop/src/app/session/hooks/use-cwd-actions.ts index 2308191b8b1b..8226a2f59501 100644 --- a/apps/desktop/src/app/session/hooks/use-cwd-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-cwd-actions.ts @@ -2,7 +2,13 @@ import { type MutableRefObject, useCallback } from 'react' import { useI18n } from '@/i18n' import { notify, notifyError } from '@/store/notifications' -import { $currentCwd, setCurrentBranch, setCurrentCwd } from '@/store/session' +import { + $currentCwd, + $newChatWorkspaceTargetGeneration, + setCurrentBranch, + setCurrentCwd, + setNewChatWorkspaceTarget +} from '@/store/session' import type { SessionRuntimeInfo } from '@/types/hermes' interface CwdActionsOptions { @@ -55,6 +61,7 @@ export function useCwdActions({ if (!activeSessionId) { setCurrentCwd(trimmed) + const workspaceGeneration = setNewChatWorkspaceTarget(trimmed) try { const info = await requestGateway<{ branch?: string; cwd?: string }>('config.get', { @@ -62,15 +69,22 @@ export function useCwdActions({ cwd: trimmed }) + if ($newChatWorkspaceTargetGeneration.get() !== workspaceGeneration || activeSessionIdRef.current) { + return + } + // Adopt the backend's normalized cwd so the persisted workspace and // branch stay consistent with what the agent will use. if (info.cwd) { setCurrentCwd(info.cwd) + setNewChatWorkspaceTarget(info.cwd) } setCurrentBranch(info.branch || '') } catch { - setCurrentBranch('') + if ($newChatWorkspaceTargetGeneration.get() === workspaceGeneration && !activeSessionIdRef.current) { + setCurrentBranch('') + } } return @@ -103,7 +117,7 @@ export function useCwdActions({ }) } }, - [activeSessionId, copy, onSessionRuntimeInfo, requestGateway] + [activeSessionId, activeSessionIdRef, copy, onSessionRuntimeInfo, requestGateway] ) return { changeSessionCwd, refreshProjectBranch } diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx index 3c1bd45e0758..3b681966cb84 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, render, waitFor } from '@testing-library/react' +import { act, cleanup, render, waitFor } from '@testing-library/react' import type { MutableRefObject } from 'react' import { useEffect } from 'react' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -11,9 +11,12 @@ import { $activeSessionId, $currentCwd, $messages, + $newChatWorkspaceTarget, $resumeFailedSessionId, setActiveSessionId, + setCurrentCwd, setMessages, + setNewChatWorkspaceTarget, setResumeFailedSessionId, setSessions } from '@/store/session' @@ -32,6 +35,7 @@ vi.mock('@/hermes', async importOriginal => ({ })) const RUNTIME_SESSION_ID = 'rt-new-001' +type HarnessHandle = Pick, 'createBackendSessionForSend' | 'startFreshSessionDraft'> function storedSession(overrides: Partial = {}): SessionInfo { return { @@ -56,7 +60,7 @@ function Harness({ onReady, requestGateway }: { - onReady: (create: (preview?: string | null) => Promise) => void + onReady: (handle: HarnessHandle) => void requestGateway: (method: string, params?: Record) => Promise }) { const ref = (value: T): MutableRefObject => ({ current: value }) @@ -79,13 +83,16 @@ function Harness({ }) useEffect(() => { - onReady(actions.createBackendSessionForSend) - }, [actions.createBackendSessionForSend, onReady]) + onReady(actions) + }, [actions, onReady]) return null } -async function createWith(profileSetup: () => void): Promise | undefined> { +async function createWith( + profileSetup: () => void, + beforeCreate?: (handle: HarnessHandle) => Promise | void +): Promise | undefined> { let createParams: Record | undefined const requestGateway = vi.fn(async (method: string, params?: Record) => { @@ -98,13 +105,23 @@ async function createWith(profileSetup: () => void): Promise Promise) | null = null - render( (create = c)} requestGateway={requestGateway} />) - await waitFor(() => expect(create).not.toBeNull()) - await create!() + let handle: HarnessHandle | null = null + render( (handle = h)} requestGateway={requestGateway} />) + await waitFor(() => expect(handle).not.toBeNull()) + + if (beforeCreate) { + await act(async () => { + await beforeCreate(handle!) + }) + } + + await act(async () => { + await handle!.createBackendSessionForSend() + }) return createParams } @@ -117,6 +134,7 @@ describe('createBackendSessionForSend profile routing', () => { $projectScope.set(ALL_PROJECTS) $projectTree.set([]) $currentCwd.set('') + setNewChatWorkspaceTarget(undefined) vi.restoreAllMocks() }) @@ -613,3 +631,44 @@ describe('resumeSession warm-cache mapping integrity', () => { expect(runtimeIdByStoredSessionIdRef.current.get('stored-A')).toBe('rt-A') }) }) + +describe('createBackendSessionForSend workspace target', () => { + afterEach(() => { + cleanup() + $newChatProfile.set(null) + $activeGatewayProfile.set('default') + setCurrentCwd('') + setNewChatWorkspaceTarget(undefined) + vi.restoreAllMocks() + }) + + it('omits cwd for an explicit no-workspace draft even when global cwd changes before send', async () => { + const params = await createWith( + () => { + $activeGatewayProfile.set('default') + }, + handle => { + handle.startFreshSessionDraft({ workspaceTarget: null }) + $currentCwd.set('/project-open-in-file-browser') + } + ) + + expect(params).not.toHaveProperty('cwd') + expect($newChatWorkspaceTarget.get()).toBeUndefined() + }) + + it('uses the clicked workspace target instead of a later global cwd value', async () => { + const params = await createWith( + () => { + $activeGatewayProfile.set('default') + }, + handle => { + handle.startFreshSessionDraft({ workspaceTarget: '/clicked-workspace' }) + $currentCwd.set('/project-open-in-file-browser') + } + ) + + expect(params).toMatchObject({ cwd: '/clicked-workspace' }) + }) + +}) diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts index ae68e7d83ce9..eec8db608117 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/index.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/index.ts @@ -18,19 +18,23 @@ import { $currentProvider, $currentReasoningEffort, $messages, + $newChatWorkspaceTarget, $sessions, $yoloActive, + type NewChatWorkspaceTarget, sessionPinId, setActiveSessionId, setAwaitingResponse, setBusy, setCurrentBranch, setCurrentCwd, + setCurrentCwdTransient, setCurrentServiceTier, setCurrentUsage, setFreshDraftReady, setIntroSeed, setMessages, + setNewChatWorkspaceTarget, setResumeExhaustedSessionId, setResumeFailedSessionId, setSelectedStoredSessionId, @@ -83,6 +87,15 @@ interface SessionActionsOptions { ) => ClientSessionState } +interface FreshSessionDraftOptions { + replaceRoute?: boolean + workspaceTarget?: NewChatWorkspaceTarget +} + +function normalizeNewChatWorkspaceTarget(target: NewChatWorkspaceTarget): NewChatWorkspaceTarget { + return typeof target === 'string' ? target.trim() || null : target +} + export function useSessionActions({ activeSessionId, activeSessionIdRef, @@ -104,7 +117,17 @@ export function useSessionActions({ const resumeRequestRef = useRef(0) const startFreshSessionDraft = useCallback( - (replaceRoute = false) => { + (options: boolean | FreshSessionDraftOptions = false) => { + const draftOptions = typeof options === 'boolean' ? { replaceRoute: options } : options + const replaceRoute = draftOptions.replaceRoute ?? false + + const hasWorkspaceTarget = + Object.hasOwn(draftOptions, 'workspaceTarget') && draftOptions.workspaceTarget !== undefined + + const workspaceTarget = hasWorkspaceTarget + ? normalizeNewChatWorkspaceTarget(draftOptions.workspaceTarget) + : undefined + busyRef.current = false setBusy(false) setAwaitingResponse(false) @@ -132,10 +155,18 @@ export function useSessionActions({ // is cleared. setCurrentServiceTier('') setYoloActive(false) - // In a project → the repo's default-branch (main worktree) checkout; not in - // a project → detached. So cmd-n "knows" the project instead of inheriting - // whatever linked worktree the last session drifted into. - setCurrentCwd(resolveNewSessionCwd()) + setNewChatWorkspaceTarget(hasWorkspaceTarget ? workspaceTarget : undefined) + + if (!hasWorkspaceTarget) { + // In a project → the repo's default-branch checkout; not in a project → + // detached. So cmd-n does not inherit an unrelated linked worktree. + setCurrentCwd(resolveNewSessionCwd()) + } else if (workspaceTarget === null) { + setCurrentCwdTransient('') + } else if (typeof workspaceTarget === 'string') { + setCurrentCwd(workspaceTarget) + } + setCurrentBranch('') // Never clear the composer here — ChatBar's per-thread draft swap owns it. setFreshDraftReady(true) @@ -162,7 +193,18 @@ export function useSessionActions({ // a backend resolves its own launch profile to None (_profile_home). const newChatProfile = $newChatProfile.get() ?? normalizeProfileKey($activeGatewayProfile.get()) await ensureGatewayProfile(newChatProfile) - const cwd = $currentCwd.get().trim() || resolveNewSessionCwd() + // An explicit one-shot workspace target (null → detached, string → that + // folder) wins; otherwise fall through to the live cwd, then the + // project-aware default (resolveNewSessionCwd). + const workspaceTarget = $newChatWorkspaceTarget.get() + + const cwd = + workspaceTarget === null + ? '' + : typeof workspaceTarget === 'string' + ? workspaceTarget.trim() + : $currentCwd.get().trim() || resolveNewSessionCwd() + // The composer's model/effort/fast is sticky UI state ($currentModel, // $currentProvider, $currentReasoningEffort, $currentFastMode). Ship it // with every session.create so the new chat opens on whatever the picker @@ -212,6 +254,7 @@ export function useSessionActions({ } setFreshDraftReady(false) + setNewChatWorkspaceTarget(undefined) setActiveSessionId(created.session_id) setSelectedStoredSessionId(stored) setSessionStartedAt(Date.now()) diff --git a/apps/desktop/src/app/session/workspace-session-target.test.ts b/apps/desktop/src/app/session/workspace-session-target.test.ts new file mode 100644 index 000000000000..3a83f605a321 --- /dev/null +++ b/apps/desktop/src/app/session/workspace-session-target.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { + $currentBranch, + $currentCwd, + $newChatWorkspaceTarget, + setCurrentBranch, + setCurrentCwd, + setNewChatWorkspaceTarget +} from '@/store/session' + +import { startWorkspaceSession } from './workspace-session-target' + +function deferred() { + let resolve!: (value: T) => void + + const promise = new Promise(done => { + resolve = done + }) + + return { promise, resolve } +} + +describe('startWorkspaceSession', () => { + afterEach(() => { + setCurrentBranch('') + setCurrentCwd('') + setNewChatWorkspaceTarget(undefined) + vi.restoreAllMocks() + }) + + it('keeps a newer sidebar target when an older project lookup resolves', async () => { + const first = deferred<{ branch?: string; cwd?: string }>() + const second = deferred<{ branch?: string; cwd?: string }>() + + const requestGateway = vi + .fn() + .mockImplementationOnce(() => first.promise) + .mockImplementationOnce(() => second.promise) + + const activeSessionIdRef = { current: null } + + const startFreshSessionDraft = vi.fn((options?: { workspaceTarget: string }) => { + setNewChatWorkspaceTarget(options?.workspaceTarget) + setCurrentCwd(options?.workspaceTarget || '') + }) + + const followActiveSessionCwd = vi.fn() + + startWorkspaceSession({ + activeSessionIdRef, + followActiveSessionCwd, + path: '/workspace-a', + requestGateway, + startFreshSessionDraft + }) + startWorkspaceSession({ + activeSessionIdRef, + followActiveSessionCwd, + path: '/workspace-b', + requestGateway, + startFreshSessionDraft + }) + + first.resolve({ branch: 'stale', cwd: '/normalized-a' }) + await first.promise + await Promise.resolve() + + expect($newChatWorkspaceTarget.get()).toBe('/workspace-b') + expect($currentCwd.get()).toBe('/workspace-b') + expect($currentBranch.get()).not.toBe('stale') + + second.resolve({ branch: 'main', cwd: '/normalized-b' }) + await second.promise + await Promise.resolve() + + expect($newChatWorkspaceTarget.get()).toBe('/normalized-b') + expect($currentCwd.get()).toBe('/normalized-b') + expect($currentBranch.get()).toBe('main') + }) +}) diff --git a/apps/desktop/src/app/session/workspace-session-target.ts b/apps/desktop/src/app/session/workspace-session-target.ts new file mode 100644 index 000000000000..da5028502a11 --- /dev/null +++ b/apps/desktop/src/app/session/workspace-session-target.ts @@ -0,0 +1,64 @@ +import type { MutableRefObject } from 'react' + +import { followActiveSessionCwd, resolveNewSessionCwd } from '@/store/projects' +import { + $newChatWorkspaceTargetGeneration, + setCurrentBranch, + setCurrentCwd, + setNewChatWorkspaceTarget +} from '@/store/session' + +interface WorkspaceSessionOptions { + activeSessionIdRef: MutableRefObject + followActiveSessionCwd?: (cwd: string) => void | Promise + onExplicitWorkspace?: (cwd: string) => void + path: null | string + requestGateway: (method: string, params?: Record) => Promise + startFreshSessionDraft: (options?: { workspaceTarget: string }) => void +} + +export function startWorkspaceSession({ + activeSessionIdRef, + followActiveSessionCwd: followCwd = followActiveSessionCwd, + onExplicitWorkspace, + path, + requestGateway, + startFreshSessionDraft +}: WorkspaceSessionOptions): void { + // A worktree lane carries its own path; a project trunk can be path-less, so + // fall back to the active project's root for that existing controller path. + const explicitTarget = path?.trim() + const target = explicitTarget || resolveNewSessionCwd() + + startFreshSessionDraft(target ? { workspaceTarget: target } : undefined) + + if (!target) { + return + } + + const workspaceGeneration = $newChatWorkspaceTargetGeneration.get() + + setCurrentCwd(target) + void requestGateway<{ branch?: string; cwd?: string }>('config.get', { key: 'project', cwd: target }) + .then(info => { + if ($newChatWorkspaceTargetGeneration.get() !== workspaceGeneration || activeSessionIdRef.current) { + return + } + + const resolved = info.cwd || target + + setCurrentCwd(resolved) + setNewChatWorkspaceTarget(resolved) + setCurrentBranch(info.branch || '') + + if (explicitTarget) { + onExplicitWorkspace?.(resolved) + void followCwd(resolved) + } + }) + .catch(() => { + if ($newChatWorkspaceTargetGeneration.get() === workspaceGeneration && !activeSessionIdRef.current) { + setCurrentBranch('') + } + }) +} diff --git a/apps/desktop/src/store/session.ts b/apps/desktop/src/store/session.ts index 2be408530541..91b27ca56afd 100644 --- a/apps/desktop/src/store/session.ts +++ b/apps/desktop/src/store/session.ts @@ -42,6 +42,7 @@ function workspaceCwdKey(connection: HermesConnection | null = $connection.get() } export const getRememberedWorkspaceCwd = (): string => storedString(workspaceCwdKey())?.trim() || '' +export type NewChatWorkspaceTarget = null | string | undefined export const getConfiguredDefaultProjectDir = (): string => configuredDefaultProjectDir @@ -270,6 +271,8 @@ export const $currentFastMode = atom(storedBoolean(COMPOSER_FAST_KEY, false)) // reflection of the truth the gateway reports rather than its own store. export const $yoloActive = atom(false) export const $currentCwd = atom(getRememberedWorkspaceCwd()) +export const $newChatWorkspaceTarget = atom(undefined) +export const $newChatWorkspaceTargetGeneration = atom(0) export const $currentBranch = atom('') export const $currentUsage = atom({ calls: 0, @@ -338,6 +341,16 @@ export const setCurrentCwd = (next: Updater) => { persistString(workspaceCwdKey(), $currentCwd.get().trim() || null) } +export const setCurrentCwdTransient = (next: Updater) => updateAtom($currentCwd, next) + +export const setNewChatWorkspaceTarget = (next: NewChatWorkspaceTarget): number => { + const generation = $newChatWorkspaceTargetGeneration.get() + 1 + $newChatWorkspaceTarget.set(next) + $newChatWorkspaceTargetGeneration.set(generation) + + return generation +} + export const workspaceCwdForNewSession = (): string => { if ($connection.get()?.mode === 'remote') { return getRememberedWorkspaceCwd()