Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
108 lines
3.2 KiB
TypeScript
108 lines
3.2 KiB
TypeScript
import type { AgentGUITargetConnectionState } from "../../../types";
|
|
import {
|
|
agentGuiScheduler,
|
|
type AgentGuiScheduledTask,
|
|
type AgentGuiScheduler
|
|
} from "../agentGuiScheduler";
|
|
|
|
export const AGENT_GUI_TARGET_CONNECTING_NOTICE_DELAY_MS = 300;
|
|
|
|
export interface AgentGUITargetConnectionBinding {
|
|
getSnapshot(): AgentGUITargetConnectionState | null;
|
|
subscribe(listener: () => void): () => void;
|
|
}
|
|
|
|
interface AgentGUITargetConnectionControllerInput {
|
|
scheduler?: AgentGuiScheduler;
|
|
source: AgentGUITargetConnectionBinding;
|
|
}
|
|
|
|
export class AgentGUITargetConnectionController {
|
|
readonly getSnapshot = (): AgentGUITargetConnectionState | null =>
|
|
this.snapshot;
|
|
readonly subscribe = (listener: () => void): (() => void) => {
|
|
this.listeners.add(listener);
|
|
if (this.listeners.size === 1) {
|
|
this.unsubscribeSource = this.source.subscribe(this.handleSourceChange);
|
|
this.reconcile();
|
|
}
|
|
return () => {
|
|
this.listeners.delete(listener);
|
|
if (this.listeners.size === 0) {
|
|
this.unsubscribeSource?.();
|
|
this.unsubscribeSource = null;
|
|
this.cancelReveal();
|
|
}
|
|
};
|
|
};
|
|
|
|
private readonly listeners = new Set<() => void>();
|
|
private readonly scheduler: AgentGuiScheduler;
|
|
private readonly source: AgentGUITargetConnectionBinding;
|
|
private revealTask: AgentGuiScheduledTask | null = null;
|
|
private snapshot: AgentGUITargetConnectionState | null;
|
|
private unsubscribeSource: (() => void) | null = null;
|
|
|
|
constructor(input: AgentGUITargetConnectionControllerInput) {
|
|
this.scheduler = input.scheduler ?? agentGuiScheduler;
|
|
this.source = input.source;
|
|
this.snapshot = this.projectImmediateStatus();
|
|
}
|
|
|
|
private readonly handleSourceChange = (): void => this.reconcile();
|
|
|
|
private reconcile(): void {
|
|
const state = this.source.getSnapshot();
|
|
if (state?.status === "unavailable") {
|
|
this.cancelReveal();
|
|
this.publish(state);
|
|
return;
|
|
}
|
|
if (state?.status !== "connecting") {
|
|
this.cancelReveal();
|
|
this.publish(null);
|
|
return;
|
|
}
|
|
if (this.snapshot?.status === "connecting") {
|
|
this.publish(state);
|
|
return;
|
|
}
|
|
if (this.revealTask) return;
|
|
this.revealTask = this.scheduler.schedule(
|
|
AGENT_GUI_TARGET_CONNECTING_NOTICE_DELAY_MS,
|
|
() => {
|
|
this.revealTask = null;
|
|
const current = this.source.getSnapshot();
|
|
if (this.listeners.size > 0 && current?.status === "connecting") {
|
|
this.publish(current);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
private projectImmediateStatus(): AgentGUITargetConnectionState | null {
|
|
const state = this.source.getSnapshot();
|
|
return state?.status === "unavailable" ? state : null;
|
|
}
|
|
|
|
private cancelReveal(): void {
|
|
this.revealTask?.cancel();
|
|
this.revealTask = null;
|
|
}
|
|
|
|
private publish(next: AgentGUITargetConnectionState | null): void {
|
|
if (targetConnectionStatesEqual(this.snapshot, next)) return;
|
|
this.snapshot = next;
|
|
for (const listener of this.listeners) listener();
|
|
}
|
|
}
|
|
|
|
function targetConnectionStatesEqual(
|
|
left: AgentGUITargetConnectionState | null,
|
|
right: AgentGUITargetConnectionState | null
|
|
): boolean {
|
|
return (
|
|
left?.status === right?.status && left?.retryAttempt === right?.retryAttempt
|
|
);
|
|
}
|