src / stateRegistry.ts
src / stateRegistry.ts
/**
* Central in-memory plugin state + status reporting.
* Includes live Current Working Directory so the model always sees it.
*/
export interface PluginState {
enabledTools: string[];
disabledTools: string[];
cacheHits: number;
cacheMisses: number;
cachedKeys: Map<string, { ttl: Date }>;
activeAgents: Map<
string,
{
name: string;
role: string;
task: string;
status: "active" | "paused" | "completed" | "failed";
parentTask?: string;
startedAt?: number;
}
>;
backgroundTasks: Map<
string,
{
command: string;
startTime: Date;
status: "running" | "completed" | "failed";
exitCode?: number | null;
}
>;
lastConfigChange: Date;
configVersion: number;
sessionStart: Date;
totalPredictions: number;
dependencyStatus?: Record<string, "available" | "unavailable">;
/** Live workspace paths – updated by toolsProvider / change_directory */
currentWorkingDirectory?: string;
workspaceRoot?: string;
}
let state: PluginState | null = null;
export function getState(): PluginState {
if (!state) {
state = {
enabledTools: [],
disabledTools: [],
cacheHits: 0,
cacheMisses: 0,
cachedKeys: new Map(),
activeAgents: new Map(),
backgroundTasks: new Map(),
lastConfigChange: new Date(),
configVersion: 1,
sessionStart: new Date(),
totalPredictions: 0,
};
}
return state;
}
/** Call from toolsProvider after resolving workspace, and after every cd. */
export function setWorkspacePaths(cwd: string, root: string): void {
const s = getState();
s.currentWorkingDirectory = cwd;
s.workspaceRoot = root;
}
export function incrementCacheHits(): void {
getState().cacheHits++;
}
export function incrementCacheMisses(): void {
getState().cacheMisses++;
}
export function addCachedKey(key: string, ttlMs: number): void {
getState().cachedKeys.set(key, { ttl: new Date(Date.now() + ttlMs) });
}
export function removeCachedKey(key: string): void {
getState().cachedKeys.delete(key);
}
export function isCacheValid(key: string): boolean {
const entry = getState().cachedKeys.get(key);
return !!entry && entry.ttl > new Date();
}
export function getStatusSummary(): string {
const s = getState();
const totalCache = s.cacheHits + s.cacheMisses;
const cacheRate =
totalCache > 0 ? Math.round((s.cacheHits / totalCache) * 100) : 0;
const activeAgents = Array.from(s.activeAgents.values()).filter(
(a) => a.status === "active",
);
const runningBg = Array.from(s.backgroundTasks.values()).filter(
(t) => t.status === "running",
);
const uptimeMin = Math.round(
(Date.now() - s.sessionStart.getTime()) / 60_000,
);
const configAgeMin = Math.round(
(Date.now() - s.lastConfigChange.getTime()) / 60_000,
);
const lines = [
"## PLUGIN STATUS",
`- **Session**: ${uptimeMin}m uptime | ${s.totalPredictions} predictions | config v${s.configVersion} (${configAgeMin}m ago)`,
];
// Always show Current working directory when known
if (s.currentWorkingDirectory) {
lines.push(`- **Current working directory**: \`${s.currentWorkingDirectory}\``);
if (
s.workspaceRoot &&
s.workspaceRoot !== s.currentWorkingDirectory
) {
lines.push(`- **Workspace root**: \`${s.workspaceRoot}\``);
}
}
lines.push(
`- **Tools**: ${s.enabledTools.length} categories active (${s.disabledTools.length} disabled)`,
`- **Cache**: ${s.cacheHits} hits / ${s.cacheMisses} misses (${cacheRate}% hit rate)`,
`- **Agents**: ${activeAgents.length} active`,
`- **Background**: ${runningBg.length} running`,
);
if (activeAgents.length > 0) {
lines.push(
...activeAgents
.slice(0, 5)
.map(
(a) =>
` · ${a.name} (${a.role}) – ${a.task.slice(0, 60)}${a.task.length > 60 ? "…" : ""}`,
),
);
}
if (runningBg.length > 0) {
lines.push(
...runningBg.slice(0, 3).map((t) => {
const secs = Math.floor((Date.now() - t.startTime.getTime()) / 1000);
return ` · "${t.command.slice(0, 50)}" (${secs}s)`;
}),
);
}
if (s.dependencyStatus) {
const missing = Object.entries(s.dependencyStatus)
.filter(([, v]) => v === "unavailable")
.map(([k]) => k);
if (missing.length > 0) {
lines.push(`- **Missing deps**: ${missing.join(", ")}`);
}
}
return lines.join("\n");
}
export function queryState(query: string): { success: boolean; data: any } {
const s = getState();
switch (query) {
case "tools":
return {
success: true,
data: { enabled: s.enabledTools, disabled: s.disabledTools },
};
case "cache":
return {
success: true,
data: {
hits: s.cacheHits,
misses: s.cacheMisses,
rate:
Math.round(
(s.cacheHits / (s.cacheHits + s.cacheMisses || 1)) * 100,
) || 0,
keys: Array.from(s.cachedKeys.keys()).slice(0, 20),
},
};
case "agents":
return {
success: true,
data: Array.from(s.activeAgents.entries()).map(([id, a]) => ({
id,
...a,
})),
};
case "background":
return {
success: true,
data: Array.from(s.backgroundTasks.entries()).map(([id, t]) => ({
id,
...t,
runningForMs: Date.now() - t.startTime.getTime(),
})),
};
case "cwd":
case "workspace":
return {
success: true,
data: {
current_working_directory: s.currentWorkingDirectory ?? null,
workspace_root: s.workspaceRoot ?? null,
},
};
case "full":
return {
success: true,
data: {
uptimeMs: Date.now() - s.sessionStart.getTime(),
totalPredictions: s.totalPredictions,
configVersion: s.configVersion,
current_working_directory: s.currentWorkingDirectory ?? null,
workspace_root: s.workspaceRoot ?? null,
cache: { hits: s.cacheHits, misses: s.cacheMisses },
agents: s.activeAgents.size,
background: s.backgroundTasks.size,
dependencyStatus: s.dependencyStatus ?? {},
},
};
default:
return { success: false, data: null };
}
}
export function registerAgent(
id: string,
info: {
name: string;
role: string;
task: string;
status?: "active" | "paused" | "completed" | "failed";
parentTask?: string;
},
): void {
getState().activeAgents.set(id, {
...info,
status: info.status ?? "active",
startedAt: Date.now(),
});
}
export function updateAgentStatus(
id: string,
status: "active" | "paused" | "completed" | "failed",
): void {
const agent = getState().activeAgents.get(id);
if (agent) agent.status = status;
}
export function registerBackgroundTask(id: string, command: string): void {
getState().backgroundTasks.set(id, {
command,
startTime: new Date(),
status: "running",
});
}
export function finishBackgroundTask(
id: string,
status: "completed" | "failed",
exitCode?: number | null,
): void {
const t = getState().backgroundTasks.get(id);
if (t) {
t.status = status;
t.exitCode = exitCode;
}
}
/**
* Central in-memory plugin state + status reporting.
* Includes live Current Working Directory so the model always sees it.
*/
export interface PluginState {
enabledTools: string[];
disabledTools: string[];
cacheHits: number;
cacheMisses: number;
cachedKeys: Map<string, { ttl: Date }>;
activeAgents: Map<
string,
{
name: string;
role: string;
task: string;
status: "active" | "paused" | "completed" | "failed";
parentTask?: string;
startedAt?: number;
}
>;
backgroundTasks: Map<
string,
{
command: string;
startTime: Date;
status: "running" | "completed" | "failed";
exitCode?: number | null;
}
>;
lastConfigChange: Date;
configVersion: number;
sessionStart: Date;
totalPredictions: number;
dependencyStatus?: Record<string, "available" | "unavailable">;
/** Live workspace paths – updated by toolsProvider / change_directory */
currentWorkingDirectory?: string;
workspaceRoot?: string;
}
let state: PluginState | null = null;
export function getState(): PluginState {
if (!state) {
state = {
enabledTools: [],
disabledTools: [],
cacheHits: 0,
cacheMisses: 0,
cachedKeys: new Map(),
activeAgents: new Map(),
backgroundTasks: new Map(),
lastConfigChange: new Date(),
configVersion: 1,
sessionStart: new Date(),
totalPredictions: 0,
};
}
return state;
}
/** Call from toolsProvider after resolving workspace, and after every cd. */
export function setWorkspacePaths(cwd: string, root: string): void {
const s = getState();
s.currentWorkingDirectory = cwd;
s.workspaceRoot = root;
}
export function incrementCacheHits(): void {
getState().cacheHits++;
}
export function incrementCacheMisses(): void {
getState().cacheMisses++;
}
export function addCachedKey(key: string, ttlMs: number): void {
getState().cachedKeys.set(key, { ttl: new Date(Date.now() + ttlMs) });
}
export function removeCachedKey(key: string): void {
getState().cachedKeys.delete(key);
}
export function isCacheValid(key: string): boolean {
const entry = getState().cachedKeys.get(key);
return !!entry && entry.ttl > new Date();
}
export function getStatusSummary(): string {
const s = getState();
const totalCache = s.cacheHits + s.cacheMisses;
const cacheRate =
totalCache > 0 ? Math.round((s.cacheHits / totalCache) * 100) : 0;
const activeAgents = Array.from(s.activeAgents.values()).filter(
(a) => a.status === "active",
);
const runningBg = Array.from(s.backgroundTasks.values()).filter(
(t) => t.status === "running",
);
const uptimeMin = Math.round(
(Date.now() - s.sessionStart.getTime()) / 60_000,
);
const configAgeMin = Math.round(
(Date.now() - s.lastConfigChange.getTime()) / 60_000,
);
const lines = [
"## PLUGIN STATUS",
`- **Session**: ${uptimeMin}m uptime | ${s.totalPredictions} predictions | config v${s.configVersion} (${configAgeMin}m ago)`,
];
// Always show Current working directory when known
if (s.currentWorkingDirectory) {
lines.push(`- **Current working directory**: \`${s.currentWorkingDirectory}\``);
if (
s.workspaceRoot &&
s.workspaceRoot !== s.currentWorkingDirectory
) {
lines.push(`- **Workspace root**: \`${s.workspaceRoot}\``);
}
}
lines.push(
`- **Tools**: ${s.enabledTools.length} categories active (${s.disabledTools.length} disabled)`,
`- **Cache**: ${s.cacheHits} hits / ${s.cacheMisses} misses (${cacheRate}% hit rate)`,
`- **Agents**: ${activeAgents.length} active`,
`- **Background**: ${runningBg.length} running`,
);
if (activeAgents.length > 0) {
lines.push(
...activeAgents
.slice(0, 5)
.map(
(a) =>
` · ${a.name} (${a.role}) – ${a.task.slice(0, 60)}${a.task.length > 60 ? "…" : ""}`,
),
);
}
if (runningBg.length > 0) {
lines.push(
...runningBg.slice(0, 3).map((t) => {
const secs = Math.floor((Date.now() - t.startTime.getTime()) / 1000);
return ` · "${t.command.slice(0, 50)}" (${secs}s)`;
}),
);
}
if (s.dependencyStatus) {
const missing = Object.entries(s.dependencyStatus)
.filter(([, v]) => v === "unavailable")
.map(([k]) => k);
if (missing.length > 0) {
lines.push(`- **Missing deps**: ${missing.join(", ")}`);
}
}
return lines.join("\n");
}
export function queryState(query: string): { success: boolean; data: any } {
const s = getState();
switch (query) {
case "tools":
return {
success: true,
data: { enabled: s.enabledTools, disabled: s.disabledTools },
};
case "cache":
return {
success: true,
data: {
hits: s.cacheHits,
misses: s.cacheMisses,
rate:
Math.round(
(s.cacheHits / (s.cacheHits + s.cacheMisses || 1)) * 100,
) || 0,
keys: Array.from(s.cachedKeys.keys()).slice(0, 20),
},
};
case "agents":
return {
success: true,
data: Array.from(s.activeAgents.entries()).map(([id, a]) => ({
id,
...a,
})),
};
case "background":
return {
success: true,
data: Array.from(s.backgroundTasks.entries()).map(([id, t]) => ({
id,
...t,
runningForMs: Date.now() - t.startTime.getTime(),
})),
};
case "cwd":
case "workspace":
return {
success: true,
data: {
current_working_directory: s.currentWorkingDirectory ?? null,
workspace_root: s.workspaceRoot ?? null,
},
};
case "full":
return {
success: true,
data: {
uptimeMs: Date.now() - s.sessionStart.getTime(),
totalPredictions: s.totalPredictions,
configVersion: s.configVersion,
current_working_directory: s.currentWorkingDirectory ?? null,
workspace_root: s.workspaceRoot ?? null,
cache: { hits: s.cacheHits, misses: s.cacheMisses },
agents: s.activeAgents.size,
background: s.backgroundTasks.size,
dependencyStatus: s.dependencyStatus ?? {},
},
};
default:
return { success: false, data: null };
}
}
export function registerAgent(
id: string,
info: {
name: string;
role: string;
task: string;
status?: "active" | "paused" | "completed" | "failed";
parentTask?: string;
},
): void {
getState().activeAgents.set(id, {
...info,
status: info.status ?? "active",
startedAt: Date.now(),
});
}
export function updateAgentStatus(
id: string,
status: "active" | "paused" | "completed" | "failed",
): void {
const agent = getState().activeAgents.get(id);
if (agent) agent.status = status;
}
export function registerBackgroundTask(id: string, command: string): void {
getState().backgroundTasks.set(id, {
command,
startTime: new Date(),
status: "running",
});
}
export function finishBackgroundTask(
id: string,
status: "completed" | "failed",
exitCode?: number | null,
): void {
const t = getState().backgroundTasks.get(id);
if (t) {
t.status = status;
t.exitCode = exitCode;
}
}