mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
fix(desktop): honor default project directory for new sessions (#43234)
* fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after #37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8f73d0d945
commit
1770263ccc
12 changed files with 263 additions and 12 deletions
|
|
@ -39,6 +39,7 @@ const {
|
|||
shouldRemoveAppBundle,
|
||||
uninstallArgsForMode
|
||||
} = require('./desktop-uninstall.cjs')
|
||||
const { isPackagedInstallPath: isPackagedInstallPathUnderRoots } = require('./workspace-cwd.cjs')
|
||||
const {
|
||||
authModeFromStatus,
|
||||
buildGatewayWsUrl,
|
||||
|
|
@ -1953,6 +1954,21 @@ function resolveRendererIndex() {
|
|||
return candidates[0]
|
||||
}
|
||||
|
||||
// True when `dir` lives inside the packaged app bundle / install tree.
|
||||
// Packaged Electron's process.cwd() (and npm's INIT_CWD when dev tooling
|
||||
// leaked into a release build) often resolve here — e.g. win-unpacked on
|
||||
// Windows — which is exactly where PR #37536 item 16 said we must NOT run.
|
||||
function isPackagedInstallPath(dir) {
|
||||
return isPackagedInstallPathUnderRoots(dir, {
|
||||
isPackaged: IS_PACKAGED,
|
||||
installRoots: [
|
||||
APP_ROOT,
|
||||
path.dirname(process.execPath),
|
||||
resolveRemovableAppPath(process.execPath, process.platform, process.env)
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function resolveHermesCwd() {
|
||||
// In a packaged build, `process.cwd()` resolves to the install root (e.g.
|
||||
// `…/win-unpacked` on Windows or `/Applications/Hermes.app/Contents/...`
|
||||
|
|
@ -1964,7 +1980,7 @@ function resolveHermesCwd() {
|
|||
const candidates = [
|
||||
readDefaultProjectDir(),
|
||||
process.env.HERMES_DESKTOP_CWD,
|
||||
process.env.INIT_CWD,
|
||||
IS_PACKAGED ? null : process.env.INIT_CWD,
|
||||
IS_PACKAGED ? null : process.cwd(),
|
||||
!IS_PACKAGED ? SOURCE_REPO_ROOT : null,
|
||||
app.getPath('home')
|
||||
|
|
@ -1973,12 +1989,37 @@ function resolveHermesCwd() {
|
|||
for (const candidate of candidates) {
|
||||
if (!candidate) continue
|
||||
const resolved = path.resolve(String(candidate))
|
||||
|
||||
if (isPackagedInstallPath(resolved)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (directoryExists(resolved)) return resolved
|
||||
}
|
||||
|
||||
return app.getPath('home')
|
||||
}
|
||||
|
||||
function sanitizeWorkspaceCwd(cwd) {
|
||||
const trimmed = typeof cwd === 'string' ? cwd.trim() : ''
|
||||
|
||||
if (!trimmed || isPackagedInstallPath(trimmed)) {
|
||||
return { cwd: resolveHermesCwd(), sanitized: Boolean(trimmed) }
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = path.resolve(trimmed)
|
||||
|
||||
if (directoryExists(resolved)) {
|
||||
return { cwd: resolved, sanitized: false }
|
||||
}
|
||||
} catch {
|
||||
// Fall through to the resolved default.
|
||||
}
|
||||
|
||||
return { cwd: resolveHermesCwd(), sanitized: Boolean(trimmed) }
|
||||
}
|
||||
|
||||
// Persisted "Default project directory" — surfaced as a setting in the
|
||||
// renderer (see app/settings/sessions-settings.tsx). Stored as JSON in
|
||||
// userData so it survives self-updates without bleeding into the new
|
||||
|
|
@ -4455,6 +4496,10 @@ async function spawnPoolBackend(profile, entry) {
|
|||
...process.env,
|
||||
HERMES_HOME,
|
||||
...backend.env,
|
||||
// Pin the gateway's tool/terminal cwd to the same directory we chose for
|
||||
// the child process. Inherited TERMINAL_CWD (or a stale config bridge)
|
||||
// can still point at the install dir even when spawn cwd is home.
|
||||
TERMINAL_CWD: hermesCwd,
|
||||
HERMES_DASHBOARD_SESSION_TOKEN: token,
|
||||
// Marks this dashboard backend as desktop-spawned so it runs the cron
|
||||
// scheduler tick loop (the gateway isn't running under the app).
|
||||
|
|
@ -4659,6 +4704,7 @@ async function startHermes() {
|
|||
// can't reliably do that, so we set it inline for every spawn.
|
||||
HERMES_HOME,
|
||||
...backend.env,
|
||||
TERMINAL_CWD: hermesCwd,
|
||||
HERMES_DASHBOARD_SESSION_TOKEN: token,
|
||||
// Marks this dashboard backend as desktop-spawned so it runs the cron
|
||||
// scheduler tick loop (the gateway isn't running under the app).
|
||||
|
|
@ -5435,9 +5481,12 @@ ipcMain.handle('hermes:openExternal', (_event, url) => {
|
|||
// session spawn (no app restart needed).
|
||||
ipcMain.handle('hermes:setting:defaultProjectDir:get', async () => ({
|
||||
dir: readDefaultProjectDir(),
|
||||
defaultLabel: path.join(app.getPath('home'), 'hermes-projects')
|
||||
defaultLabel: app.getPath('home'),
|
||||
resolvedCwd: resolveHermesCwd()
|
||||
}))
|
||||
|
||||
ipcMain.handle('hermes:workspace:sanitize', async (_event, cwd) => sanitizeWorkspaceCwd(cwd))
|
||||
|
||||
ipcMain.handle('hermes:setting:defaultProjectDir:set', async (_event, dir) => {
|
||||
const next = typeof dir === 'string' && dir.trim() ? dir.trim() : null
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
|
|||
setPreviewShortcutActive: active => ipcRenderer.send('hermes:previewShortcutActive', Boolean(active)),
|
||||
openExternal: url => ipcRenderer.invoke('hermes:openExternal', url),
|
||||
fetchLinkTitle: url => ipcRenderer.invoke('hermes:fetchLinkTitle', url),
|
||||
sanitizeWorkspaceCwd: cwd => ipcRenderer.invoke('hermes:workspace:sanitize', cwd),
|
||||
settings: {
|
||||
getDefaultProjectDir: () => ipcRenderer.invoke('hermes:setting:defaultProjectDir:get'),
|
||||
setDefaultProjectDir: dir => ipcRenderer.invoke('hermes:setting:defaultProjectDir:set', dir),
|
||||
|
|
|
|||
38
apps/desktop/electron/workspace-cwd.cjs
Normal file
38
apps/desktop/electron/workspace-cwd.cjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
const path = require('node:path')
|
||||
|
||||
/** True when `dir` lives inside a packaged app bundle / install tree. */
|
||||
function isPackagedInstallPath(dir, { installRoots, isPackaged }) {
|
||||
if (!isPackaged || !dir) {
|
||||
return false
|
||||
}
|
||||
|
||||
let resolved
|
||||
|
||||
try {
|
||||
resolved = path.resolve(String(dir))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
const roots = new Set(
|
||||
(installRoots ?? [])
|
||||
.filter(Boolean)
|
||||
.map(candidate => path.resolve(String(candidate)))
|
||||
)
|
||||
|
||||
for (const root of roots) {
|
||||
if (resolved === root) {
|
||||
return true
|
||||
}
|
||||
|
||||
const rel = path.relative(root, resolved)
|
||||
|
||||
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
module.exports = { isPackagedInstallPath }
|
||||
45
apps/desktop/electron/workspace-cwd.test.cjs
Normal file
45
apps/desktop/electron/workspace-cwd.test.cjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Tests for electron/workspace-cwd.cjs.
|
||||
*
|
||||
* Run with: node --test electron/workspace-cwd.test.cjs
|
||||
*/
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const path = require('node:path')
|
||||
|
||||
const { isPackagedInstallPath } = require('./workspace-cwd.cjs')
|
||||
|
||||
const installRoot = path.resolve('/opt/Hermes')
|
||||
|
||||
test('isPackagedInstallPath returns false when not packaged', () => {
|
||||
assert.equal(
|
||||
isPackagedInstallPath(installRoot, { isPackaged: false, installRoots: [installRoot] }),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
test('isPackagedInstallPath flags the install root itself', () => {
|
||||
assert.equal(
|
||||
isPackagedInstallPath(installRoot, { isPackaged: true, installRoots: [installRoot] }),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('isPackagedInstallPath flags paths nested under the install root', () => {
|
||||
const nested = path.join(installRoot, 'resources', 'app.asar')
|
||||
|
||||
assert.equal(
|
||||
isPackagedInstallPath(nested, { isPackaged: true, installRoots: [installRoot] }),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
test('isPackagedInstallPath ignores paths outside the install root', () => {
|
||||
const homeProject = path.resolve('/home/user/projects/demo')
|
||||
|
||||
assert.equal(
|
||||
isPackagedInstallPath(homeProject, { isPackaged: true, installRoots: [installRoot] }),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
"test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
|
||||
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
|
||||
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-probes.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/workspace-cwd.test.cjs",
|
||||
"type-check": "tsc -b",
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
$connection,
|
||||
$sessions,
|
||||
$workingSessionIds,
|
||||
ensureDefaultWorkspaceCwd,
|
||||
setConnection,
|
||||
setSessionsLoading
|
||||
} from '@/store/session'
|
||||
|
|
@ -351,6 +352,7 @@ export function useGatewayBoot({
|
|||
message: translateNow('boot.steps.loadingSettings'),
|
||||
progress: 97
|
||||
})
|
||||
await ensureDefaultWorkspaceCwd()
|
||||
await callbacksRef.current.refreshHermesConfig()
|
||||
|
||||
if (cancelled) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
$sessions,
|
||||
$yoloActive,
|
||||
getRememberedWorkspaceCwd,
|
||||
workspaceCwdForNewSession,
|
||||
sessionPinId,
|
||||
setActiveSessionId,
|
||||
setAwaitingResponse,
|
||||
|
|
@ -311,8 +312,9 @@ export function useSessionActions({
|
|||
})
|
||||
setSessionStartedAt(null)
|
||||
setTurnStartedAt(null)
|
||||
// New chats inherit the current workspace.
|
||||
setCurrentCwd(getRememberedWorkspaceCwd())
|
||||
// New chats start in the configured default project dir when set,
|
||||
// otherwise the sticky last-used workspace (PR #37586).
|
||||
setCurrentCwd(workspaceCwdForNewSession())
|
||||
setCurrentBranch('')
|
||||
clearComposerDraft()
|
||||
clearComposerAttachments()
|
||||
|
|
@ -333,7 +335,7 @@ export function useSessionActions({
|
|||
// Route the new chat to the chosen profile's backend (null = primary,
|
||||
// so single-profile users are unaffected).
|
||||
await ensureGatewayProfile($newChatProfile.get())
|
||||
const cwd = $currentCwd.get().trim() || getRememberedWorkspaceCwd()
|
||||
const cwd = $currentCwd.get().trim() || workspaceCwdForNewSession()
|
||||
// Pass the owning profile so a new chat under a non-launch profile (global
|
||||
// remote mode) builds its agent + persists against THAT profile's home/db.
|
||||
const newChatProfile = $newChatProfile.get()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { sessionTitle } from '@/lib/chat-runtime'
|
|||
import { triggerHaptic } from '@/lib/haptics'
|
||||
import { Archive, ArchiveOff, FolderOpen, Loader2, Trash2 } from '@/lib/icons'
|
||||
import { notify, notifyError } from '@/store/notifications'
|
||||
import { setSessions } from '@/store/session'
|
||||
import { applyConfiguredDefaultProjectDir, ensureDefaultWorkspaceCwd, setSessions } from '@/store/session'
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import { EmptyState, ListRow, LoadingState, SectionHeading, SettingsContent } from './primitives'
|
||||
|
|
@ -196,6 +196,7 @@ function DefaultProjectDirSetting() {
|
|||
|
||||
setDir(result.dir)
|
||||
setFallback(result.defaultLabel)
|
||||
applyConfiguredDefaultProjectDir(result.dir)
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
|
@ -221,7 +222,8 @@ function DefaultProjectDirSetting() {
|
|||
|
||||
const result = await settings.setDefaultProjectDir(picked.dir)
|
||||
setDir(result.dir)
|
||||
notify({ durationMs: 2_000, kind: 'success', message: s.defaultDirUpdated })
|
||||
applyConfiguredDefaultProjectDir(result.dir)
|
||||
notify({ durationMs: 4_000, kind: 'success', message: s.defaultDirUpdated })
|
||||
} catch (err) {
|
||||
notifyError(err, s.updateDirFailed)
|
||||
} finally {
|
||||
|
|
@ -241,6 +243,8 @@ function DefaultProjectDirSetting() {
|
|||
try {
|
||||
await settings.setDefaultProjectDir(null)
|
||||
setDir(null)
|
||||
applyConfiguredDefaultProjectDir(null)
|
||||
await ensureDefaultWorkspaceCwd()
|
||||
} catch (err) {
|
||||
notifyError(err, s.clearDirFailed)
|
||||
} finally {
|
||||
|
|
@ -268,7 +272,7 @@ function DefaultProjectDirSetting() {
|
|||
)}
|
||||
</div>
|
||||
}
|
||||
description={dir || s.defaultsTo(fallback || '~/hermes-projects')}
|
||||
description={dir || s.defaultsTo(fallback || '~')}
|
||||
title={dir ? dir : s.notSet}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
3
apps/desktop/src/global.d.ts
vendored
3
apps/desktop/src/global.d.ts
vendored
|
|
@ -55,8 +55,9 @@ declare global {
|
|||
setPreviewShortcutActive?: (active: boolean) => void
|
||||
openExternal: (url: string) => Promise<void>
|
||||
fetchLinkTitle: (url: string) => Promise<string>
|
||||
sanitizeWorkspaceCwd: (cwd?: null | string) => Promise<{ cwd: string; sanitized: boolean }>
|
||||
settings: {
|
||||
getDefaultProjectDir: () => Promise<{ defaultLabel: string; dir: null | string }>
|
||||
getDefaultProjectDir: () => Promise<{ defaultLabel: string; dir: null | string; resolvedCwd: string }>
|
||||
pickDefaultProjectDir: () => Promise<{ canceled: boolean; dir: null | string }>
|
||||
setDefaultProjectDir: (dir: null | string) => Promise<{ dir: null | string }>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ export const en: Translations = {
|
|||
defaultDirTitle: 'Default project directory',
|
||||
defaultDirDesc:
|
||||
'New sessions start in this folder unless you pick another. Leave it unset to use your home directory.',
|
||||
defaultDirUpdated: 'Default project directory updated',
|
||||
defaultDirUpdated: 'Default project directory updated — start a new chat (Ctrl/⌘+N) for it to take effect',
|
||||
defaultsTo: label => `Defaults to ${label}.`,
|
||||
change: 'Change',
|
||||
choose: 'Choose',
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import {
|
||||
$activeSessionId,
|
||||
$attentionSessionIds,
|
||||
$currentCwd,
|
||||
$workingSessionIds,
|
||||
applyConfiguredDefaultProjectDir,
|
||||
getRecentlySettledSessionIds,
|
||||
mergeSessionPage,
|
||||
sessionPinId,
|
||||
setSessionAttention,
|
||||
setSessionWorking
|
||||
setSessionWorking,
|
||||
workspaceCwdForNewSession
|
||||
} from './session'
|
||||
|
||||
const session = (over: Partial<SessionInfo>): SessionInfo => ({
|
||||
|
|
@ -138,6 +142,43 @@ describe('mergeSessionPage', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('workspaceCwdForNewSession', () => {
|
||||
afterEach(() => {
|
||||
applyConfiguredDefaultProjectDir(null)
|
||||
$currentCwd.set('')
|
||||
$activeSessionId.set(null)
|
||||
window.localStorage.removeItem('hermes.desktop.workspace-cwd')
|
||||
})
|
||||
|
||||
it('prefers the configured default over the sticky remembered workspace', () => {
|
||||
window.localStorage.setItem('hermes.desktop.workspace-cwd', '/home/user/sticky')
|
||||
applyConfiguredDefaultProjectDir('/home/user/configured')
|
||||
|
||||
expect(workspaceCwdForNewSession()).toBe('/home/user/configured')
|
||||
})
|
||||
|
||||
it('falls back to the remembered workspace when no configured default is set', () => {
|
||||
window.localStorage.setItem('hermes.desktop.workspace-cwd', '/home/user/sticky')
|
||||
|
||||
expect(workspaceCwdForNewSession()).toBe('/home/user/sticky')
|
||||
})
|
||||
|
||||
it('falls back to the live cwd when neither configured nor remembered values exist', () => {
|
||||
$currentCwd.set('/home/user/live')
|
||||
|
||||
expect(workspaceCwdForNewSession()).toBe('/home/user/live')
|
||||
})
|
||||
|
||||
it('does not rewrite the live cwd while a session is active', () => {
|
||||
$activeSessionId.set('sess-1')
|
||||
$currentCwd.set('/live/session/path')
|
||||
applyConfiguredDefaultProjectDir('/home/user/configured')
|
||||
|
||||
expect($currentCwd.get()).toBe('/live/session/path')
|
||||
expect(workspaceCwdForNewSession()).toBe('/home/user/configured')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRecentlySettledSessionIds', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
|
|
|
|||
|
|
@ -10,8 +10,71 @@ type Updater<T> = T | ((current: T) => T)
|
|||
|
||||
const WORKSPACE_CWD_KEY = 'hermes.desktop.workspace-cwd'
|
||||
|
||||
// Cached copy of Settings → Sessions → Default project directory. The main
|
||||
// process persists this in project-dir.json, but the renderer must also honor it
|
||||
// when seeding $currentCwd — otherwise PR #37586's sticky localStorage home dir
|
||||
// wins and new sessions ignore the user's explicit picker choice.
|
||||
let configuredDefaultProjectDir = ''
|
||||
|
||||
export const getRememberedWorkspaceCwd = (): string => storedString(WORKSPACE_CWD_KEY)?.trim() || ''
|
||||
|
||||
export const getConfiguredDefaultProjectDir = (): string => configuredDefaultProjectDir
|
||||
|
||||
export async function syncConfiguredDefaultProjectDir(): Promise<string> {
|
||||
const settings = window.hermesDesktop?.settings?.getDefaultProjectDir
|
||||
|
||||
if (!settings) {
|
||||
configuredDefaultProjectDir = ''
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const { dir } = await settings()
|
||||
configuredDefaultProjectDir = dir?.trim() || ''
|
||||
|
||||
return configuredDefaultProjectDir
|
||||
}
|
||||
|
||||
/** Align the renderer workspace with the main-process default (home dir when
|
||||
* packaged, optional Settings override). Clears stale install-dir paths that
|
||||
* PR #37586's localStorage stickiness can preserve across the #37536 fix. */
|
||||
export async function ensureDefaultWorkspaceCwd(): Promise<void> {
|
||||
const sanitize = window.hermesDesktop?.sanitizeWorkspaceCwd
|
||||
|
||||
if (!sanitize) {
|
||||
return
|
||||
}
|
||||
|
||||
await syncConfiguredDefaultProjectDir()
|
||||
const configured = getConfiguredDefaultProjectDir()
|
||||
|
||||
const seedLiveCwd = (cwd: string) => {
|
||||
if (cwd && !$activeSessionId.get()) {
|
||||
setCurrentCwd(cwd)
|
||||
}
|
||||
}
|
||||
|
||||
if (configured) {
|
||||
const { cwd } = await sanitize(configured)
|
||||
seedLiveCwd(cwd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const { cwd } = await sanitize(getRememberedWorkspaceCwd())
|
||||
seedLiveCwd(cwd)
|
||||
}
|
||||
|
||||
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> {
|
||||
get: () => T
|
||||
set: (value: T) => void
|
||||
|
|
@ -171,6 +234,11 @@ export const setCurrentCwd = (next: Updater<string>) => {
|
|||
persistString(WORKSPACE_CWD_KEY, $currentCwd.get().trim() || null)
|
||||
}
|
||||
|
||||
/** Workspace for a brand-new chat. Explicit Settings override wins; otherwise
|
||||
* fall back to the sticky last-used folder, then whatever is already live. */
|
||||
export const workspaceCwdForNewSession = (): string =>
|
||||
getConfiguredDefaultProjectDir() || getRememberedWorkspaceCwd() || $currentCwd.get().trim()
|
||||
|
||||
export const setCurrentBranch = (next: Updater<string>) => updateAtom($currentBranch, next)
|
||||
export const setCurrentUsage = (next: Updater<UsageStats>) => updateAtom($currentUsage, next)
|
||||
export const setSessionStartedAt = (next: Updater<number | null>) => updateAtom($sessionStartedAt, next)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue