Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/happy-cli/src/claude/claudeLocalLauncher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ export async function claudeLocalLauncher(session: Session): Promise<LauncherRes
};
session.addSessionFoundCallback(scannerSessionCallback);

// Register callback to update scanner when Claude changes cwd (e.g. worktree)
const scannerCwdCallback = (newCwd: string) => {
scanner.updateWorkingDirectory(newCwd);
};
session.addCwdChangeCallback(scannerCwdCallback);


// Handle abort
let exitReason: LauncherResult | null = null;
Expand Down Expand Up @@ -160,6 +166,7 @@ export async function claudeLocalLauncher(session: Session): Promise<LauncherRes

// Remove session found callback
session.removeSessionFoundCallback(scannerSessionCallback);
session.removeCwdChangeCallback(scannerCwdCallback);

// Cleanup
await scanner.cleanup();
Expand Down
8 changes: 7 additions & 1 deletion packages/happy-cli/src/claude/runClaude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export async function runClaude(credentials: Credentials, options: StartOptions
const hookServer = await startHookServer({
onSessionHook: (sessionId, data) => {
logger.debug(`[START] Session hook received: ${sessionId}`, data);

// Update session ID in the Session instance
if (currentSession) {
const previousSessionId = currentSession.sessionId;
Expand All @@ -202,6 +202,12 @@ export async function runClaude(credentials: Credentials, options: StartOptions
currentSession.onSessionFound(sessionId);
}
}

// Update working directory when Claude changes cwd (e.g. worktree)
if (data.cwd && data.cwd !== workingDirectory) {
logger.debug(`[START] Claude cwd changed: ${workingDirectory} -> ${data.cwd}`);
currentSession?.onCwdChange(data.cwd);
}
}
});
logger.debug(`[START] Hook server started on port ${hookServer.port}`);
Expand Down
32 changes: 32 additions & 0 deletions packages/happy-cli/src/claude/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export class Session {

/** Callbacks to be notified when session ID is found/changed */
private sessionFoundCallbacks: ((sessionId: string) => void)[] = [];

/** Callbacks to be notified when Claude changes cwd (e.g. worktree) */
private cwdChangeCallbacks: ((newCwd: string) => void)[] = [];

/** Keep alive interval reference for cleanup */
private keepAliveInterval: NodeJS.Timeout;
Expand Down Expand Up @@ -78,6 +81,7 @@ export class Session {
cleanup = (): void => {
clearInterval(this.keepAliveInterval);
this.sessionFoundCallbacks = [];
this.cwdChangeCallbacks = [];
logger.debug('[Session] Cleaned up resources');
}

Expand Down Expand Up @@ -136,6 +140,34 @@ export class Session {
}
}

/**
* Called when Claude's working directory changes (e.g. entering a worktree).
* Notifies all registered callbacks so the session scanner can update its path.
*/
onCwdChange = (newCwd: string) => {
logger.debug(`[Session] Claude cwd changed to ${newCwd}`);
for (const callback of this.cwdChangeCallbacks) {
callback(newCwd);
}
}

/**
* Register a callback to be notified when Claude's cwd changes
*/
addCwdChangeCallback = (callback: (newCwd: string) => void): void => {
this.cwdChangeCallbacks.push(callback);
}

/**
* Remove a cwd change callback
*/
removeCwdChangeCallback = (callback: (newCwd: string) => void): void => {
const index = this.cwdChangeCallbacks.indexOf(callback);
if (index !== -1) {
this.cwdChangeCallbacks.splice(index, 1);
}
}

/**
* Clear the current session ID (used by /clear command)
*/
Expand Down
17 changes: 15 additions & 2 deletions packages/happy-cli/src/claude/utils/sessionScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export async function createSessionScanner(opts: {
onMessage: (message: RawJSONLines) => void
}) {

// Resolve project directory
const projectDir = getProjectPath(opts.workingDirectory);
// Resolve project directory (mutable — updated when worktree changes cwd)
let projectDir = getProjectPath(opts.workingDirectory);

// Finished, pending finishing and current session
let finishedSessions = new Set<string>();
Expand Down Expand Up @@ -118,6 +118,19 @@ export async function createSessionScanner(opts: {
await sync.invalidateAndAwait();
sync.stop();
},
/**
* Update the working directory when Claude changes cwd (e.g. worktree).
* Stops old watchers pointing to the wrong directory and re-syncs.
*/
updateWorkingDirectory: (newCwd: string) => {
const newProjectDir = getProjectPath(newCwd);
if (newProjectDir === projectDir) return;
logger.debug(`[SESSION_SCANNER] Working directory changed, updating projectDir: ${projectDir} -> ${newProjectDir}`);
for (const w of watchers.values()) w();
watchers.clear();
projectDir = newProjectDir;
sync.invalidate();
},
onNewSession: (sessionId: string) => {
if (currentSessionId === sessionId) {
logger.debug(`[SESSION_SCANNER] New session: ${sessionId} is the same as the current session, skipping`);
Expand Down