mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Merge pull request #74938 from NousResearch/bb/rail-own-worktree
fix(desktop): a session's coding rail follows its own worktree
This commit is contained in:
commit
3a2b332985
4 changed files with 115 additions and 24 deletions
|
|
@ -16,8 +16,6 @@ import { DiffCount } from '@/components/ui/diff-count'
|
|||
import type { HermesGitBranch } from '@/global'
|
||||
import { useI18n } from '@/i18n'
|
||||
import {
|
||||
$repoStatus,
|
||||
$repoWorktrees,
|
||||
registerRepoStatusCwd,
|
||||
repoStatusForCwd,
|
||||
repoWorktreesForCwd
|
||||
|
|
@ -69,10 +67,13 @@ export const CodingStatusRow = memo(function CodingStatusRow({
|
|||
const s = t.statusStack.coding
|
||||
const p = t.sidebar.projects
|
||||
const resolvedRepoPath = repoPath?.trim() || undefined
|
||||
// Per-cwd slice when this surface knows its worktree (tiles); otherwise the
|
||||
// primary main-pane computed — so a blank/missing repoPath still paints.
|
||||
const status = useStore(resolvedRepoPath ? repoStatusForCwd(resolvedRepoPath) : $repoStatus)
|
||||
const worktrees = useStore(resolvedRepoPath ? repoWorktreesForCwd(resolvedRepoPath) : $repoWorktrees)
|
||||
// This surface's OWN worktree, always — never the primary's. The row used to
|
||||
// fall back to the global `$repoStatus` for a blank repoPath, which painted
|
||||
// the main pane's branch/± onto a tile whose cwd hadn't resolved yet. That
|
||||
// fallback bought nothing (the primary's computed is keyed to `$currentCwd`,
|
||||
// which is blank in exactly the same case) and cost a wrong-tree rail.
|
||||
const status = useStore(repoStatusForCwd(resolvedRepoPath))
|
||||
const worktrees = useStore(repoWorktreesForCwd(resolvedRepoPath))
|
||||
|
||||
// While mounted, keep this worktree in the coding-status refresh set so the
|
||||
// turn-settle / tool-complete / focus edges re-probe it too (tiles otherwise
|
||||
|
|
|
|||
|
|
@ -504,7 +504,9 @@ export function useSessionActions({
|
|||
upsertOptimisticSession(created, stored, null, null)
|
||||
}
|
||||
|
||||
const runtimeInfo = applyRuntimeInfo(created.info)
|
||||
// A tile lives in its OWN worktree — it must not publish its cwd/branch
|
||||
// into the composer atoms the main pane renders from.
|
||||
const runtimeInfo = applyRuntimeInfo(created.info, { foreground: false })
|
||||
updateSessionState(created.session_id, state => (runtimeInfo ? { ...state, ...runtimeInfo } : state), stored)
|
||||
|
||||
openSessionTile(stored, dir)
|
||||
|
|
@ -1182,7 +1184,9 @@ export function useSessionActions({
|
|||
routedSessionId
|
||||
)
|
||||
|
||||
const runtimeInfo = applyRuntimeInfo(branched.info)
|
||||
// The branch opens as its own tile in the parent's worktree, not as the
|
||||
// primary session — keep its runtime out of the main composer atoms.
|
||||
const runtimeInfo = applyRuntimeInfo(branched.info, { foreground: false })
|
||||
patchSessionWorkspace(routedSessionId, runtimeInfo?.cwd)
|
||||
|
||||
if (runtimeInfo) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { ChatMessage } from '@/lib/chat-messages'
|
|||
import { $approvalModes, approvalModeForProfile } from '@/store/approval-mode'
|
||||
import { $desktopOnboarding } from '@/store/onboarding'
|
||||
import { $activeGatewayProfile } from '@/store/profile'
|
||||
import { $currentBranch, $currentCwd, setCurrentBranch, setCurrentCwd } from '@/store/session'
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import {
|
||||
|
|
@ -65,6 +66,36 @@ describe('applyRuntimeInfo credential warnings', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('applyRuntimeInfo foreground scoping', () => {
|
||||
beforeEach(() => {
|
||||
setCurrentCwd('/main-repo')
|
||||
setCurrentBranch('main')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setCurrentCwd('')
|
||||
setCurrentBranch('')
|
||||
})
|
||||
|
||||
it('publishes a foreground runtime into the composer atoms', () => {
|
||||
const patch = applyRuntimeInfo({ branch: 'bb/feature', cwd: '/main-repo/worktree' })
|
||||
|
||||
expect($currentCwd.get()).toBe('/main-repo/worktree')
|
||||
expect($currentBranch.get()).toBe('bb/feature')
|
||||
expect(patch).toMatchObject({ branch: 'bb/feature', cwd: '/main-repo/worktree' })
|
||||
})
|
||||
|
||||
it('keeps a background runtime out of the composer atoms but still returns its patch', () => {
|
||||
const patch = applyRuntimeInfo({ branch: 'bb/tile', cwd: '/other-worktree' }, { foreground: false })
|
||||
|
||||
// The main pane's rail must stay on its own tree.
|
||||
expect($currentCwd.get()).toBe('/main-repo')
|
||||
expect($currentBranch.get()).toBe('main')
|
||||
// ...while the caller still gets everything it needs for its own session.
|
||||
expect(patch).toMatchObject({ branch: 'bb/tile', cwd: '/other-worktree' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSessionGoneError', () => {
|
||||
it('is true for 404 / session-not-found, false otherwise', () => {
|
||||
expect(isSessionGoneError(new Error('Request failed 404'))).toBe(true)
|
||||
|
|
|
|||
|
|
@ -706,13 +706,72 @@ type SessionRuntimeStatePatch = Partial<
|
|||
>
|
||||
>
|
||||
|
||||
export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionRuntimeStatePatch | null {
|
||||
interface ApplyRuntimeInfoOptions {
|
||||
/**
|
||||
* Whether this runtime belongs to the session the MAIN pane is showing.
|
||||
* Foreground (the default) mirrors into the composer atoms every main-pane
|
||||
* surface reads.
|
||||
*
|
||||
* A tile or a background branch must pass `false`: it owns a different
|
||||
* worktree, and writing its cwd into `$currentCwd` re-pointed the main
|
||||
* composer's coding rail (and the persisted workspace cwd) at the tile's
|
||||
* repo — the main rail painted a branch from a tree its session was never
|
||||
* in. The returned patch still carries every field, so the caller's own
|
||||
* per-session state is unaffected.
|
||||
*/
|
||||
foreground?: boolean
|
||||
}
|
||||
|
||||
/** Mirror a session's runtime state into the composer atoms the MAIN pane
|
||||
* renders from. Foreground sessions only — see ApplyRuntimeInfoOptions. */
|
||||
function publishRuntimeToComposer(state: SessionRuntimeStatePatch): void {
|
||||
if (state.model !== undefined) {
|
||||
setCurrentModel(state.model)
|
||||
}
|
||||
|
||||
if (state.provider !== undefined) {
|
||||
setCurrentProvider(state.provider)
|
||||
}
|
||||
|
||||
if (state.cwd !== undefined) {
|
||||
setCurrentCwd(state.cwd)
|
||||
}
|
||||
|
||||
if (state.branch !== undefined) {
|
||||
setCurrentBranch(state.branch)
|
||||
}
|
||||
|
||||
if (state.personality !== undefined) {
|
||||
setCurrentPersonality(state.personality)
|
||||
}
|
||||
|
||||
if (state.reasoningEffort !== undefined) {
|
||||
setCurrentReasoningEffort(state.reasoningEffort)
|
||||
}
|
||||
|
||||
if (state.serviceTier !== undefined) {
|
||||
setCurrentServiceTier(state.serviceTier)
|
||||
}
|
||||
|
||||
if (state.fast !== undefined) {
|
||||
setCurrentFastMode(state.fast)
|
||||
}
|
||||
|
||||
if (state.yolo !== undefined) {
|
||||
setYoloActive(state.yolo)
|
||||
}
|
||||
}
|
||||
|
||||
export function applyRuntimeInfo(
|
||||
info: SessionRuntimeInfo | undefined,
|
||||
{ foreground = true }: ApplyRuntimeInfoOptions = {}
|
||||
): SessionRuntimeStatePatch | null {
|
||||
if (!info) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sessionState: SessionRuntimeStatePatch = {}
|
||||
|
||||
// App/profile-level reporting is session-independent — a tile's runtime
|
||||
// reports backend skew and credential warnings just as usefully.
|
||||
reportBackendContract(info.desktop_contract)
|
||||
|
||||
if (info.approval_mode !== undefined) {
|
||||
|
|
@ -723,54 +782,50 @@ export function applyRuntimeInfo(info: SessionRuntimeInfo | undefined): SessionR
|
|||
|
||||
reportInstallMethodWarning(info.install_warning)
|
||||
|
||||
const sessionState: SessionRuntimeStatePatch = {}
|
||||
|
||||
if (typeof info.model === 'string') {
|
||||
setCurrentModel(info.model)
|
||||
sessionState.model = info.model
|
||||
}
|
||||
|
||||
if (typeof info.provider === 'string') {
|
||||
setCurrentProvider(info.provider)
|
||||
sessionState.provider = info.provider
|
||||
}
|
||||
|
||||
if (info.cwd) {
|
||||
setCurrentCwd(info.cwd)
|
||||
sessionState.cwd = info.cwd
|
||||
}
|
||||
|
||||
if (info.branch !== undefined) {
|
||||
setCurrentBranch(info.branch || '')
|
||||
sessionState.branch = info.branch || ''
|
||||
}
|
||||
|
||||
if (typeof info.personality === 'string') {
|
||||
const personality = normalizePersonalityValue(info.personality)
|
||||
setCurrentPersonality(personality)
|
||||
sessionState.personality = personality
|
||||
sessionState.personality = normalizePersonalityValue(info.personality)
|
||||
}
|
||||
|
||||
if (typeof info.reasoning_effort === 'string') {
|
||||
setCurrentReasoningEffort(info.reasoning_effort)
|
||||
sessionState.reasoningEffort = info.reasoning_effort
|
||||
}
|
||||
|
||||
if (typeof info.service_tier === 'string') {
|
||||
setCurrentServiceTier(info.service_tier)
|
||||
sessionState.serviceTier = info.service_tier
|
||||
}
|
||||
|
||||
if (typeof info.fast === 'boolean') {
|
||||
setCurrentFastMode(info.fast)
|
||||
sessionState.fast = info.fast
|
||||
}
|
||||
|
||||
if (typeof info.yolo === 'boolean') {
|
||||
setYoloActive(info.yolo)
|
||||
sessionState.yolo = info.yolo
|
||||
}
|
||||
|
||||
if (info.usage) {
|
||||
setCurrentUsage(current => ({ ...current, ...info.usage }))
|
||||
if (foreground) {
|
||||
publishRuntimeToComposer(sessionState)
|
||||
|
||||
if (info.usage) {
|
||||
setCurrentUsage(current => ({ ...current, ...info.usage }))
|
||||
}
|
||||
}
|
||||
|
||||
return sessionState
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue