fix: restart idle sidecar on capability refresh

Force-refreshing sidecar capabilities now disposes the idle sidecar process before re-querying capabilities, so Kopaya can pick up backend sidecar changes and refreshed Copilot connection state without a full app restart.

Add a small helper and regression tests for the refresh decision.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-22 15:29:59 +01:00
co-authored by Copilot
parent a77bd107ab
commit 9cded003b5
4 changed files with 41 additions and 1 deletions
+6 -1
View File
@@ -496,7 +496,12 @@ export class KopayaAppService extends EventEmitter<AppServiceEvents> {
}
private async loadSidecarCapabilities(forceRefresh = false): Promise<SidecarCapabilities> {
if (forceRefresh || !this.sidecarCapabilities) {
if (forceRefresh) {
this.sidecarCapabilities = await this.sidecar.refreshCapabilities();
return this.sidecarCapabilities;
}
if (!this.sidecarCapabilities) {
this.sidecarCapabilities = await this.sidecar.describeCapabilities();
}
+19
View File
@@ -12,6 +12,7 @@ import type {
} from '@shared/contracts/sidecar';
import type { ChatMessageRecord } from '@shared/domain/session';
import { createSidecarEnvironment } from '@main/sidecar/sidecarEnvironment';
import { shouldRestartSidecarOnCapabilityRefresh } from '@main/sidecar/sidecarRefresh';
import {
markRunTurnPendingErrored,
shouldHandleRunTurnEvent,
@@ -46,6 +47,14 @@ export class SidecarClient {
return command;
}
async refreshCapabilities(): Promise<SidecarCapabilities> {
if (shouldRestartSidecarOnCapabilityRefresh(this.hasActiveRunTurn())) {
await this.dispose();
}
return this.describeCapabilities();
}
async validatePattern(pattern: ValidatePatternCommand['pattern']): Promise<unknown> {
return this.dispatch<unknown>({
type: 'validate-pattern',
@@ -71,6 +80,16 @@ export class SidecarClient {
this.process = undefined;
}
hasActiveRunTurn(): boolean {
for (const pending of this.pending.values()) {
if (pending.kind === 'run-turn') {
return true;
}
}
return false;
}
private async ensureProcess(): Promise<ChildProcessWithoutNullStreams> {
if (this.process && !this.process.killed) {
return this.process;
+3
View File
@@ -0,0 +1,3 @@
export function shouldRestartSidecarOnCapabilityRefresh(hasActiveRunTurn: boolean): boolean {
return !hasActiveRunTurn;
}
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, test } from 'bun:test';
import { shouldRestartSidecarOnCapabilityRefresh } from '@main/sidecar/sidecarRefresh';
describe('shouldRestartSidecarOnCapabilityRefresh', () => {
test('restarts the sidecar when no run-turn is active', () => {
expect(shouldRestartSidecarOnCapabilityRefresh(false)).toBe(true);
});
test('keeps the existing sidecar when a run-turn is active', () => {
expect(shouldRestartSidecarOnCapabilityRefresh(true)).toBe(false);
});
});