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.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
import { createContext, useContext, type JSX, type ReactNode } from "react";
|
|
import {
|
|
useExternalStoreSnapshot,
|
|
type ExternalStoreSnapshotSource
|
|
} from "@tutti-os/ui-react-hooks";
|
|
|
|
const AgentConversationClockVisibilityContext = createContext(true);
|
|
type ClockListener = () => void;
|
|
|
|
class AgentConversationClockStore {
|
|
private readonly minuteListeners = new Set<ClockListener>();
|
|
private readonly secondListeners = new Set<ClockListener>();
|
|
private minuteTimeMs = Date.now();
|
|
private minuteTimer: number | null = null;
|
|
private secondTimeMs = Date.now();
|
|
private secondTimer: number | null = null;
|
|
|
|
readonly disabled: ExternalStoreSnapshotSource<number> = {
|
|
getSnapshot: () => 0,
|
|
subscribe: () => () => undefined
|
|
};
|
|
|
|
readonly minute: ExternalStoreSnapshotSource<number> = {
|
|
getSnapshot: () => this.minuteTimeMs,
|
|
subscribe: (listener) =>
|
|
this.subscribe(
|
|
listener,
|
|
this.minuteListeners,
|
|
60_000,
|
|
"minuteTimeMs",
|
|
"minuteTimer"
|
|
)
|
|
};
|
|
|
|
readonly second: ExternalStoreSnapshotSource<number> = {
|
|
getSnapshot: () => this.secondTimeMs,
|
|
subscribe: (listener) =>
|
|
this.subscribe(
|
|
listener,
|
|
this.secondListeners,
|
|
1_000,
|
|
"secondTimeMs",
|
|
"secondTimer"
|
|
)
|
|
};
|
|
|
|
private subscribe(
|
|
listener: ClockListener,
|
|
listeners: Set<ClockListener>,
|
|
intervalMs: number,
|
|
timeKey: "minuteTimeMs" | "secondTimeMs",
|
|
timerKey: "minuteTimer" | "secondTimer"
|
|
): () => void {
|
|
listeners.add(listener);
|
|
if (listeners.size === 1) {
|
|
this[timeKey] = Date.now();
|
|
// timing: share one cadence timer across all mounted consumers.
|
|
this[timerKey] = window.setInterval(() => {
|
|
this[timeKey] = Date.now();
|
|
listeners.forEach((candidate) => candidate());
|
|
}, intervalMs);
|
|
}
|
|
return () => {
|
|
listeners.delete(listener);
|
|
if (listeners.size === 0) {
|
|
const timer = this[timerKey];
|
|
if (timer !== null) {
|
|
window.clearInterval(timer);
|
|
this[timerKey] = null;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
const agentConversationClock = new AgentConversationClockStore();
|
|
|
|
export function AgentConversationClockProvider({
|
|
children,
|
|
isVisible
|
|
}: {
|
|
children: ReactNode;
|
|
isVisible: boolean;
|
|
}): JSX.Element {
|
|
return (
|
|
<AgentConversationClockVisibilityContext.Provider value={isVisible}>
|
|
{children}
|
|
</AgentConversationClockVisibilityContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAgentConversationNowUnixMs(enabled: boolean): number | null {
|
|
const isVisible = useContext(AgentConversationClockVisibilityContext);
|
|
const shouldTick = enabled && isVisible;
|
|
const nowUnixMs = useExternalStoreSnapshot(
|
|
shouldTick ? agentConversationClock.second : agentConversationClock.disabled
|
|
);
|
|
return shouldTick ? nowUnixMs : null;
|
|
}
|
|
|
|
export function useAgentConversationMinuteNowUnixMs(): number {
|
|
const isVisible = useContext(AgentConversationClockVisibilityContext);
|
|
const nowUnixMs = useExternalStoreSnapshot(
|
|
isVisible ? agentConversationClock.minute : agentConversationClock.disabled
|
|
);
|
|
return isVisible ? nowUnixMs : agentConversationClock.minute.getSnapshot();
|
|
}
|