From 9cded003b598149a837931cae2c8421ffcd2f841 Mon Sep 17 00:00:00 2001 From: David Kaya Date: Sun, 22 Mar 2026 15:29:59 +0100 Subject: [PATCH] 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> --- src/main/KopayaAppService.ts | 7 ++++++- src/main/sidecar/sidecarProcess.ts | 19 +++++++++++++++++++ src/main/sidecar/sidecarRefresh.ts | 3 +++ tests/main/sidecarRefresh.test.ts | 13 +++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/main/sidecar/sidecarRefresh.ts create mode 100644 tests/main/sidecarRefresh.test.ts diff --git a/src/main/KopayaAppService.ts b/src/main/KopayaAppService.ts index 64bdc93..a7241b3 100644 --- a/src/main/KopayaAppService.ts +++ b/src/main/KopayaAppService.ts @@ -496,7 +496,12 @@ export class KopayaAppService extends EventEmitter { } private async loadSidecarCapabilities(forceRefresh = false): Promise { - if (forceRefresh || !this.sidecarCapabilities) { + if (forceRefresh) { + this.sidecarCapabilities = await this.sidecar.refreshCapabilities(); + return this.sidecarCapabilities; + } + + if (!this.sidecarCapabilities) { this.sidecarCapabilities = await this.sidecar.describeCapabilities(); } diff --git a/src/main/sidecar/sidecarProcess.ts b/src/main/sidecar/sidecarProcess.ts index cf075c0..bf0f745 100644 --- a/src/main/sidecar/sidecarProcess.ts +++ b/src/main/sidecar/sidecarProcess.ts @@ -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 { + if (shouldRestartSidecarOnCapabilityRefresh(this.hasActiveRunTurn())) { + await this.dispose(); + } + + return this.describeCapabilities(); + } + async validatePattern(pattern: ValidatePatternCommand['pattern']): Promise { return this.dispatch({ 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 { if (this.process && !this.process.killed) { return this.process; diff --git a/src/main/sidecar/sidecarRefresh.ts b/src/main/sidecar/sidecarRefresh.ts new file mode 100644 index 0000000..0ddd9b2 --- /dev/null +++ b/src/main/sidecar/sidecarRefresh.ts @@ -0,0 +1,3 @@ +export function shouldRestartSidecarOnCapabilityRefresh(hasActiveRunTurn: boolean): boolean { + return !hasActiveRunTurn; +} diff --git a/tests/main/sidecarRefresh.test.ts b/tests/main/sidecarRefresh.test.ts new file mode 100644 index 0000000..15b0e68 --- /dev/null +++ b/tests/main/sidecarRefresh.test.ts @@ -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); + }); +});