diff --git a/desktop/src/components/chat/ChatInput.tsx b/desktop/src/components/chat/ChatInput.tsx index 2f7945ac..2a261af1 100644 --- a/desktop/src/components/chat/ChatInput.tsx +++ b/desktop/src/components/chat/ChatInput.tsx @@ -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([]) diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts index 6683b94a..934ac879 100644 --- a/src/server/__tests__/sessions.test.ts +++ b/src/server/__tests__/sessions.test.ts @@ -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 { 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 { + 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 + + 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 + + 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()]) diff --git a/src/server/api/sessions.ts b/src/server/api/sessions.ts index f9077cea..1bc26ee9 100644 --- a/src/server/api/sessions.ts +++ b/src/server/api/sessions.ts @@ -534,10 +534,12 @@ function chooseRicherUsage( } async function getGitInfo(sessionId: string): Promise { - 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 { 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 { } catch { // Not a git repo or git not available return Response.json({ - branch: null, + branch: sessionBranch, repoName: null, workDir, changedFiles: 0,