mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: keep session git badge on the active CLI checkout
The chat footer previously read git-info from the persisted session file and refreshed only on tab changes. Repository launches can update the active CLI cwd after the composer has already rendered, so the footer could keep showing an older selected branch even while the running model was operating in the correct checkout. Constraint: The active CLI process is the source of truth for a live session cwd Rejected: Derive the footer branch only from persisted session metadata | metadata can lag behind startup and does not prove the current running cwd Confidence: high Scope-risk: narrow Tested: bun test src/server/__tests__/sessions.test.ts --test-name-pattern "git-info should prefer" Tested: cd desktop && bun run lint Tested: git diff --check
This commit is contained in:
parent
8533bc0bf6
commit
678ba18a6f
@ -138,6 +138,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
],
|
||||
[attachments, workspaceReferences],
|
||||
)
|
||||
const slashCommandCount = slashCommands.length
|
||||
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus()
|
||||
@ -174,7 +175,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
})
|
||||
}, [composerPrefill])
|
||||
|
||||
useEffect(() => {
|
||||
const refreshGitInfo = useCallback(() => {
|
||||
if (!activeTabId) {
|
||||
setGitInfo(null)
|
||||
return
|
||||
@ -186,6 +187,16 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
|
||||
sessionsApi.getGitInfo(activeTabId).then(setGitInfo).catch(() => setGitInfo(null))
|
||||
}, [activeTabId, isMemberSession])
|
||||
|
||||
useEffect(() => {
|
||||
refreshGitInfo()
|
||||
}, [refreshGitInfo])
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeTabId || isMemberSession || messageCount === 0) return
|
||||
const timeout = setTimeout(refreshGitInfo, chatState === 'idle' ? 0 : 500)
|
||||
return () => clearTimeout(timeout)
|
||||
}, [activeTabId, chatState, isMemberSession, messageCount, refreshGitInfo, slashCommandCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMemberSession) return
|
||||
setAttachments([])
|
||||
|
||||
@ -7,8 +7,7 @@ import * as fs from 'node:fs/promises'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import * as path from 'node:path'
|
||||
import * as os from 'node:os'
|
||||
import { SessionService } from '../services/sessionService.js'
|
||||
import { sessionService } from '../services/sessionService.js'
|
||||
import { SessionService, sessionService } from '../services/sessionService.js'
|
||||
import {
|
||||
getRepositoryContext,
|
||||
prepareSessionWorkspace,
|
||||
@ -39,6 +38,36 @@ async function cleanupTmpDir(): Promise<void> {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
}
|
||||
|
||||
function git(cwd: string, ...args: string[]): string {
|
||||
return execFileSync('git', args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
|
||||
async function createCleanGitRepo(baseDir: string): Promise<string> {
|
||||
const workDir = path.join(
|
||||
baseDir,
|
||||
`repo-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||
)
|
||||
|
||||
await fs.mkdir(workDir, { recursive: true })
|
||||
git(workDir, 'init')
|
||||
git(workDir, 'config', 'user.email', 'sessions-api@example.com')
|
||||
git(workDir, 'config', 'user.name', 'Sessions API')
|
||||
git(workDir, 'checkout', '-b', 'main')
|
||||
await fs.writeFile(path.join(workDir, 'README.md'), 'main\n')
|
||||
git(workDir, 'add', 'README.md')
|
||||
git(workDir, 'commit', '-m', 'initial')
|
||||
git(workDir, 'checkout', '-b', 'feature/rail')
|
||||
await fs.writeFile(path.join(workDir, 'feature.txt'), 'feature\n')
|
||||
git(workDir, 'add', 'feature.txt')
|
||||
git(workDir, 'commit', '-m', 'feature')
|
||||
git(workDir, 'checkout', 'main')
|
||||
|
||||
return workDir
|
||||
}
|
||||
|
||||
/** Write a JSONL session file with given entries. */
|
||||
async function writeSessionFile(
|
||||
projectDir: string,
|
||||
@ -1437,6 +1466,48 @@ describe('Sessions API', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('GET /api/sessions/:id/git-info should prefer the active CLI workDir', async () => {
|
||||
const workDir = await createCleanGitRepo(tmpDir)
|
||||
const activeWorktree = path.join(tmpDir, `active-feature-rail-${Date.now()}`)
|
||||
git(workDir, 'worktree', 'add', activeWorktree, 'feature/rail')
|
||||
const { sessionId } = await sessionService.createSession(workDir)
|
||||
const sessionsMap = (conversationService as any).sessions as Map<string, { workDir: string }>
|
||||
|
||||
sessionsMap.set(sessionId, { workDir: activeWorktree })
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/sessions/${sessionId}/git-info`)
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const body = (await res.json()) as { branch: string | null; workDir: string }
|
||||
expect(body.workDir).toBe(activeWorktree)
|
||||
expect(body.branch).toBe('feature/rail')
|
||||
} finally {
|
||||
sessionsMap.delete(sessionId)
|
||||
}
|
||||
})
|
||||
|
||||
it('GET /api/sessions/:id/git-info should keep the session launch branch stable', async () => {
|
||||
const workDir = await createCleanGitRepo(tmpDir)
|
||||
const { sessionId } = await sessionService.createSession(
|
||||
workDir,
|
||||
{ branch: 'feature/rail', worktree: false },
|
||||
)
|
||||
const sessionsMap = (conversationService as any).sessions as Map<string, { workDir: string }>
|
||||
|
||||
sessionsMap.set(sessionId, { workDir })
|
||||
git(workDir, 'switch', 'main')
|
||||
try {
|
||||
const res = await fetch(`${baseUrl}/api/sessions/${sessionId}/git-info`)
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const body = (await res.json()) as { branch: string | null; workDir: string }
|
||||
expect(body.workDir).toBe(workDir)
|
||||
expect(body.branch).toBe('feature/rail')
|
||||
} finally {
|
||||
sessionsMap.delete(sessionId)
|
||||
}
|
||||
})
|
||||
|
||||
it('DELETE /api/sessions/:id should delete the session', async () => {
|
||||
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
await writeSessionFile('-tmp-api-test', sessionId, [makeSnapshotEntry()])
|
||||
|
||||
@ -534,10 +534,12 @@ function chooseRicherUsage(
|
||||
}
|
||||
|
||||
async function getGitInfo(sessionId: string): Promise<Response> {
|
||||
const workDir = await sessionService.getSessionWorkDir(sessionId)
|
||||
const workDir = conversationService.getSessionWorkDir(sessionId) || await sessionService.getSessionWorkDir(sessionId)
|
||||
if (!workDir) {
|
||||
throw ApiError.notFound(`Session not found: ${sessionId}`)
|
||||
}
|
||||
const launchInfo = await sessionService.getSessionLaunchInfo(sessionId).catch(() => null)
|
||||
const sessionBranch = launchInfo?.repository?.branch || null
|
||||
|
||||
try {
|
||||
// Get branch name
|
||||
@ -547,7 +549,7 @@ async function getGitInfo(sessionId: string): Promise<Response> {
|
||||
stderr: 'pipe',
|
||||
})
|
||||
const branchText = await new Response(branchProc.stdout).text()
|
||||
const branch = branchText.trim()
|
||||
const branch = sessionBranch || branchText.trim()
|
||||
|
||||
// Get repo name from remote or directory
|
||||
let repoName = ''
|
||||
@ -586,7 +588,7 @@ async function getGitInfo(sessionId: string): Promise<Response> {
|
||||
} catch {
|
||||
// Not a git repo or git not available
|
||||
return Response.json({
|
||||
branch: null,
|
||||
branch: sessionBranch,
|
||||
repoName: null,
|
||||
workDir,
|
||||
changedFiles: 0,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user