refactor(renderer): split snapshot state sections
This commit is contained in:
1 parent
236714e17a
commit
1383805d66
12 files changed
+1030
-899
No files matched your search
@@ -3,6 +3,8 @@ import type { ProfileSnapshot, SectionId } from "@shared/types";
|
||||
import { describeDesktopError, unwrap } from "../lib/desktop";
|
||||
import { useAppStore } from "../state/store";
|
||||
|
||||
export type SnapshotRefreshGuard = () => boolean;
|
||||
|
||||
export function useDataLoader(
|
||||
snapshot: ProfileSnapshot | null,
|
||||
section: SectionId,
|
||||
@@ -11,10 +13,11 @@ export function useDataLoader(
|
||||
// selectedRunId via getState() at call time so its identity stays stable
|
||||
// across connector/run selection changes and AppActions consumers do not
|
||||
// cascade re-render.
|
||||
const refresh = useCallback(async () => {
|
||||
const refresh = useCallback(async (shouldApply: SnapshotRefreshGuard = () => true) => {
|
||||
const store = useAppStore.getState();
|
||||
try {
|
||||
const next = unwrap<ProfileSnapshot>(await window.agentCompany.loadSnapshot());
|
||||
if (!shouldApply()) return;
|
||||
store.setSnapshot(next);
|
||||
if (next.companies.length === 0) {
|
||||
store.setInitialOnboardingLocked(true);
|
||||
@@ -26,12 +29,15 @@ export function useDataLoader(
|
||||
if (next.currentCompanyId) {
|
||||
store.setSocialAccounts(next.socialAccounts?.filter((a) => a.companyId === next.currentCompanyId) ?? []);
|
||||
const browserActionResult = await window.agentCompany.listBrowserActions({ companyId: next.currentCompanyId, limit: 50 });
|
||||
if (!shouldApply()) return;
|
||||
if (browserActionResult.ok) {
|
||||
store.setBrowserActions(browserActionResult.data);
|
||||
}
|
||||
}
|
||||
if (!shouldApply()) return;
|
||||
store.setError(null);
|
||||
} catch (nextError) {
|
||||
if (!shouldApply()) return;
|
||||
store.setError(describeDesktopError(nextError));
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -1,37 +1,44 @@
|
||||
import { useEffect, type MutableRefObject } from "react";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
import {
|
||||
createDomainEventBatcher,
|
||||
isSnapshotIrrelevantEvent,
|
||||
classifySnapshotEvent,
|
||||
createSnapshotEventGuard,
|
||||
type GranularEventEntry,
|
||||
} from "../lib/ipc-batcher";
|
||||
} from "../lib/snapshot-events";
|
||||
import { useAppStore } from "../state/store";
|
||||
|
||||
export function useDesktopEvents(
|
||||
refreshRef: MutableRefObject<() => Promise<void>>,
|
||||
refreshRef: MutableRefObject<(shouldApply?: () => boolean) => Promise<void>>,
|
||||
logBufferRef: MutableRefObject<Record<string, string>>,
|
||||
incrementLogRevision: () => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
void refreshRef.current();
|
||||
const store = useAppStore;
|
||||
const eventGuard = createSnapshotEventGuard();
|
||||
|
||||
async function handleGranularBatch(events: GranularEventEntry[]) {
|
||||
function requestFullRefresh() {
|
||||
eventGuard.invalidateSnapshot();
|
||||
const token = eventGuard.captureSnapshot();
|
||||
void refreshRef.current(() => eventGuard.canApplySnapshot(token));
|
||||
}
|
||||
|
||||
requestFullRefresh();
|
||||
|
||||
async function handleSnapshotEntityEvent(entry: GranularEventEntry) {
|
||||
const companyId = store.getState().snapshot?.currentCompanyId ?? null;
|
||||
if (!companyId) {
|
||||
void refreshRef.current();
|
||||
requestFullRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
let needsFullRefresh = false;
|
||||
eventGuard.invalidateSnapshot();
|
||||
eventGuard.invalidateEntity(entry);
|
||||
const token = eventGuard.captureEntity(entry);
|
||||
|
||||
for (const entry of events) {
|
||||
// run-updated:completed touches too many entity types -- full refresh
|
||||
if (entry.type === "run-updated" && entry.action === "completed") {
|
||||
needsFullRefresh = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.type === "run-updated" && entry.action === "completed") {
|
||||
needsFullRefresh = true;
|
||||
} else {
|
||||
try {
|
||||
switch (entry.type) {
|
||||
case "task-updated": {
|
||||
@@ -39,6 +46,7 @@ export function useDesktopEvents(
|
||||
store.getState().removeTask(entry.entityId);
|
||||
} else {
|
||||
const result = await window.agentCompany.getTask({ companyId, id: entry.entityId });
|
||||
if (!eventGuard.canApplyEntity(entry, token)) return;
|
||||
if (result.ok) {
|
||||
store.getState().patchTask(result.data);
|
||||
} else {
|
||||
@@ -52,6 +60,7 @@ export function useDesktopEvents(
|
||||
store.getState().removeRun(entry.entityId);
|
||||
} else {
|
||||
const result = await window.agentCompany.getRun({ companyId, id: entry.entityId });
|
||||
if (!eventGuard.canApplyEntity(entry, token)) return;
|
||||
if (result.ok) {
|
||||
store.getState().patchRun(result.data);
|
||||
} else {
|
||||
@@ -65,6 +74,7 @@ export function useDesktopEvents(
|
||||
store.getState().removeApproval(entry.entityId);
|
||||
} else {
|
||||
const result = await window.agentCompany.getApproval({ companyId, id: entry.entityId });
|
||||
if (!eventGuard.canApplyEntity(entry, token)) return;
|
||||
if (result.ok) {
|
||||
store.getState().patchApproval(result.data);
|
||||
} else {
|
||||
@@ -75,6 +85,7 @@ export function useDesktopEvents(
|
||||
}
|
||||
case "agent-updated": {
|
||||
const result = await window.agentCompany.getAgent({ companyId, id: entry.entityId });
|
||||
if (!eventGuard.canApplyEntity(entry, token)) return;
|
||||
if (result.ok) {
|
||||
store.getState().patchAgent(result.data);
|
||||
} else {
|
||||
@@ -83,44 +94,24 @@ export function useDesktopEvents(
|
||||
break;
|
||||
}
|
||||
case "budget-changed": {
|
||||
// Budget is a computed aggregate -- full refresh is needed
|
||||
needsFullRefresh = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// Unknown granular event type -- fall back (per D-05 / PERF-03)
|
||||
needsFullRefresh = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[renderer/useDesktopEvents] granular fetch failed for ${entry.type}/${entry.entityId}; falling back to full refresh: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
} catch {
|
||||
needsFullRefresh = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (needsFullRefresh) break;
|
||||
}
|
||||
|
||||
if (needsFullRefresh) {
|
||||
void refreshRef.current();
|
||||
requestFullRefresh();
|
||||
}
|
||||
// Granular patches already update the snapshot arrays, and
|
||||
// sidebar badges derive from the snapshot live — no separate
|
||||
// badge refresh is needed.
|
||||
}
|
||||
|
||||
// 500ms window for full-refresh events (e.g. new-message,
|
||||
// thread-wait-state-updated) so bursts coalesce into one reload.
|
||||
// Granular entity events still flush at this window but are cheap.
|
||||
const batcher = createDomainEventBatcher(
|
||||
() => { void refreshRef.current(); },
|
||||
(events) => { void handleGranularBatch(events); },
|
||||
500,
|
||||
);
|
||||
|
||||
const unsubscribe = window.agentCompany.subscribe((event: DesktopEvent) => {
|
||||
if (event.type === "run-log") {
|
||||
const MAX_LOG_BYTES_PER_RUN = 512_000;
|
||||
@@ -141,24 +132,27 @@ export function useDesktopEvents(
|
||||
}
|
||||
if (event.type === "shortcut") {
|
||||
if (event.action === "toggle-command-palette") store.getState().toggleCommandPalette();
|
||||
if (event.action === "refresh") void refreshRef.current();
|
||||
if (event.action === "refresh") requestFullRefresh();
|
||||
return;
|
||||
}
|
||||
if (event.type === "notification-click") {
|
||||
store.getState().navigateTo(event.section, event.entityId);
|
||||
return;
|
||||
}
|
||||
// One-shot events that do not affect ProfileSnapshot -- handled by
|
||||
// their own listeners elsewhere, never trigger a snapshot refresh.
|
||||
if (isSnapshotIrrelevantEvent(event)) {
|
||||
|
||||
const decision = classifySnapshotEvent(event);
|
||||
if (decision.kind === "ignore") {
|
||||
return;
|
||||
}
|
||||
// All remaining data-mutating events go through the enhanced batcher
|
||||
batcher.push(event);
|
||||
if (decision.kind === "full-refresh") {
|
||||
requestFullRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
void handleSnapshotEntityEvent(decision.entry);
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
batcher.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- refreshRef and logBufferRef are stable refs; incrementLogRevision is a stable Zustand setter
|
||||
}, []);
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
|
||||
export interface GranularEventEntry {
|
||||
type: string;
|
||||
entityId: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
const MAX_GRANULAR_EVENTS = 10;
|
||||
|
||||
export function entityKeyFromEvent(event: DesktopEvent): GranularEventEntry | null {
|
||||
switch (event.type) {
|
||||
case "task-updated":
|
||||
return { type: "task-updated", entityId: event.taskId, action: event.action };
|
||||
case "run-updated":
|
||||
return { type: "run-updated", entityId: event.runId, action: event.action };
|
||||
case "approval-updated":
|
||||
return { type: "approval-updated", entityId: event.approvalId, action: event.action };
|
||||
case "agent-updated":
|
||||
return { type: "agent-updated", entityId: event.agentId, action: event.action };
|
||||
case "budget-changed":
|
||||
return { type: "budget-changed", entityId: event.companyId, action: "changed" };
|
||||
// run-status is a run-level state change -- map onto run entity so
|
||||
// granular patchRun refetches the run record.
|
||||
case "run-status":
|
||||
return { type: "run-updated", entityId: event.runId, action: "status_changed" };
|
||||
// heartbeat-status mutates agent metadata (nextWakeAt / enabled) -- map
|
||||
// onto agent entity so granular patchAgent refetches.
|
||||
case "heartbeat-status":
|
||||
return { type: "agent-updated", entityId: event.agentId, action: "updated" };
|
||||
// agent-message posts a new comment under a task; patchTask refetch will
|
||||
// pick up the task's updated comment list.
|
||||
case "agent-message":
|
||||
return { type: "task-updated", entityId: event.taskId, action: "updated" };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Events that do NOT affect ProfileSnapshot and therefore must not trigger
|
||||
// a snapshot refresh -- they are handled exclusively by their own listeners.
|
||||
const SNAPSHOT_IRRELEVANT_EVENT_TYPES = new Set<DesktopEvent["type"]>([
|
||||
"automation-fired",
|
||||
"recovery-needed",
|
||||
]);
|
||||
|
||||
export function isSnapshotIrrelevantEvent(event: DesktopEvent): boolean {
|
||||
return SNAPSHOT_IRRELEVANT_EVENT_TYPES.has(event.type);
|
||||
}
|
||||
|
||||
export function createDomainEventBatcher(
|
||||
onFullRefresh: () => void,
|
||||
onGranularBatch: (events: GranularEventEntry[]) => void,
|
||||
windowMs = 50,
|
||||
maxGranular = MAX_GRANULAR_EVENTS,
|
||||
) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let hasDomainChanged = false;
|
||||
const pending = new Map<string, GranularEventEntry>();
|
||||
|
||||
function push(event: DesktopEvent) {
|
||||
if (event.type === "domain-changed") {
|
||||
hasDomainChanged = true;
|
||||
} else {
|
||||
const entry = entityKeyFromEvent(event);
|
||||
if (entry) {
|
||||
const key = `${entry.type}:${entry.entityId}`;
|
||||
const existing = pending.get(key);
|
||||
// If any event in window has action "deleted", preserve that action
|
||||
if (existing && existing.action === "deleted" && entry.action !== "deleted") {
|
||||
// Keep the delete -- it takes priority
|
||||
} else {
|
||||
pending.set(key, entry);
|
||||
}
|
||||
} else {
|
||||
// Unknown event type -- treat as domain-changed for safety (per D-05)
|
||||
hasDomainChanged = true;
|
||||
}
|
||||
}
|
||||
if (timer === null) {
|
||||
timer = setTimeout(flushInternal, windowMs);
|
||||
}
|
||||
}
|
||||
|
||||
function flushInternal() {
|
||||
timer = null;
|
||||
if (hasDomainChanged || pending.size > maxGranular) {
|
||||
onFullRefresh();
|
||||
} else if (pending.size > 0) {
|
||||
onGranularBatch([...pending.values()]);
|
||||
}
|
||||
hasDomainChanged = false;
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
function flush() {
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
flushInternal();
|
||||
}
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
hasDomainChanged = false;
|
||||
pending.clear();
|
||||
}
|
||||
|
||||
return { push, flush, dispose };
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
|
||||
export type SnapshotEntityEventType =
|
||||
| "task-updated"
|
||||
| "run-updated"
|
||||
| "approval-updated"
|
||||
| "agent-updated"
|
||||
| "budget-changed";
|
||||
|
||||
export interface SnapshotEntityEvent {
|
||||
type: SnapshotEntityEventType;
|
||||
entityId: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export type GranularEventEntry = SnapshotEntityEvent;
|
||||
|
||||
export type SnapshotEventDecision =
|
||||
| { kind: "entity"; entry: SnapshotEntityEvent }
|
||||
| { kind: "full-refresh" }
|
||||
| { kind: "ignore" };
|
||||
|
||||
export interface SnapshotEntityToken {
|
||||
snapshotEpoch: number;
|
||||
entityVersion: number;
|
||||
}
|
||||
|
||||
export interface SnapshotRefreshToken {
|
||||
snapshotEpoch: number;
|
||||
}
|
||||
|
||||
export interface SnapshotEventGuard {
|
||||
invalidateSnapshot: () => void;
|
||||
captureSnapshot: () => SnapshotRefreshToken;
|
||||
canApplySnapshot: (token: SnapshotRefreshToken) => boolean;
|
||||
invalidateEntity: (entry: SnapshotEntityEvent) => void;
|
||||
captureEntity: (entry: SnapshotEntityEvent) => SnapshotEntityToken;
|
||||
canApplyEntity: (entry: SnapshotEntityEvent, token: SnapshotEntityToken) => boolean;
|
||||
}
|
||||
|
||||
export function classifySnapshotEvent(event: DesktopEvent): SnapshotEventDecision {
|
||||
switch (event.type) {
|
||||
case "task-updated":
|
||||
return { kind: "entity", entry: { type: "task-updated", entityId: event.taskId, action: event.action } };
|
||||
case "run-updated":
|
||||
return { kind: "entity", entry: { type: "run-updated", entityId: event.runId, action: event.action } };
|
||||
case "approval-updated":
|
||||
return {
|
||||
kind: "entity",
|
||||
entry: { type: "approval-updated", entityId: event.approvalId, action: event.action },
|
||||
};
|
||||
case "agent-updated":
|
||||
return { kind: "entity", entry: { type: "agent-updated", entityId: event.agentId, action: event.action } };
|
||||
case "budget-changed":
|
||||
return { kind: "entity", entry: { type: "budget-changed", entityId: event.companyId, action: "changed" } };
|
||||
case "run-status":
|
||||
return { kind: "entity", entry: { type: "run-updated", entityId: event.runId, action: "status_changed" } };
|
||||
case "heartbeat-status":
|
||||
return { kind: "entity", entry: { type: "agent-updated", entityId: event.agentId, action: "updated" } };
|
||||
case "agent-message":
|
||||
return { kind: "entity", entry: { type: "task-updated", entityId: event.taskId, action: "updated" } };
|
||||
case "automation-fired":
|
||||
case "recovery-needed":
|
||||
case "run-log":
|
||||
case "shortcut":
|
||||
case "notification-click":
|
||||
return { kind: "ignore" };
|
||||
case "domain-changed":
|
||||
case "new-message":
|
||||
case "thread-wait-state-updated":
|
||||
case "credentials-changed":
|
||||
case "agent-event":
|
||||
case "oauth:status":
|
||||
case "oauth:auth-url":
|
||||
case "oauth:progress":
|
||||
case "oauth:prompt-request":
|
||||
case "oauth:manual-code-request":
|
||||
return { kind: "full-refresh" };
|
||||
default:
|
||||
return { kind: "full-refresh" };
|
||||
}
|
||||
}
|
||||
|
||||
export function createSnapshotEventGuard(): SnapshotEventGuard {
|
||||
let snapshotEpoch = 0;
|
||||
const entityVersions = new Map<string, number>();
|
||||
|
||||
function entityKey(entry: SnapshotEntityEvent): string {
|
||||
return `${entry.type}:${entry.entityId}`;
|
||||
}
|
||||
|
||||
function entityVersion(entry: SnapshotEntityEvent): number {
|
||||
return entityVersions.get(entityKey(entry)) ?? 0;
|
||||
}
|
||||
|
||||
return {
|
||||
invalidateSnapshot() {
|
||||
snapshotEpoch += 1;
|
||||
},
|
||||
captureSnapshot() {
|
||||
return { snapshotEpoch };
|
||||
},
|
||||
canApplySnapshot(token) {
|
||||
return snapshotEpoch === token.snapshotEpoch;
|
||||
},
|
||||
invalidateEntity(entry) {
|
||||
const key = entityKey(entry);
|
||||
entityVersions.set(key, (entityVersions.get(key) ?? 0) + 1);
|
||||
},
|
||||
captureEntity(entry) {
|
||||
return {
|
||||
snapshotEpoch,
|
||||
entityVersion: entityVersion(entry),
|
||||
};
|
||||
},
|
||||
canApplyEntity(entry, token) {
|
||||
return snapshotEpoch === token.snapshotEpoch && entityVersion(entry) === token.entityVersion;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
type AppErrorInput,
|
||||
type AppErrorNotice,
|
||||
} from "../../lib/desktop";
|
||||
import {
|
||||
assembleProfileSnapshot,
|
||||
splitProfileSnapshot,
|
||||
type ProfileSnapshotSections,
|
||||
} from "../snapshot-sections";
|
||||
|
||||
// Sidebar badge counts and inbox items both live in the renderer —
|
||||
// derived on every read from the snapshot (see `@shared/sidebar-badges`
|
||||
@@ -24,6 +29,7 @@ import {
|
||||
// slice is what let the counters drift against the rendered content.
|
||||
export interface CompanySlice {
|
||||
snapshot: ProfileSnapshot | null;
|
||||
snapshotSections: ProfileSnapshotSections | null;
|
||||
/**
|
||||
* Bumps only on full-snapshot replacements (setSnapshot / refreshSnapshot).
|
||||
* Granular patch/remove methods DO NOT bump this counter so that
|
||||
@@ -49,6 +55,14 @@ export interface CompanySlice {
|
||||
removeApproval: (approvalId: string) => void;
|
||||
}
|
||||
|
||||
function applySnapshotSections(
|
||||
state: CompanySlice,
|
||||
sections: ProfileSnapshotSections | null,
|
||||
) {
|
||||
state.snapshotSections = sections;
|
||||
state.snapshot = sections ? assembleProfileSnapshot(sections) : null;
|
||||
}
|
||||
|
||||
export const createCompanySlice: StateCreator<
|
||||
AppState,
|
||||
[["zustand/immer", never]],
|
||||
@@ -56,13 +70,14 @@ export const createCompanySlice: StateCreator<
|
||||
CompanySlice
|
||||
> = (set) => ({
|
||||
snapshot: null,
|
||||
snapshotSections: null,
|
||||
snapshotRevision: 0,
|
||||
companyMetrics: null,
|
||||
standupReport: null,
|
||||
error: null,
|
||||
setSnapshot: (snapshot) =>
|
||||
set((state) => {
|
||||
state.snapshot = snapshot;
|
||||
applySnapshotSections(state, snapshot ? splitProfileSnapshot(snapshot) : null);
|
||||
state.snapshotRevision += 1;
|
||||
}),
|
||||
setCompanyMetrics: (metrics) =>
|
||||
@@ -82,7 +97,7 @@ export const createCompanySlice: StateCreator<
|
||||
const result = await window.agentCompany.loadSnapshot();
|
||||
if (result.ok) {
|
||||
set((state) => {
|
||||
state.snapshot = result.data;
|
||||
applySnapshotSections(state, splitProfileSnapshot(result.data));
|
||||
state.snapshotRevision += 1;
|
||||
});
|
||||
} else {
|
||||
@@ -101,65 +116,85 @@ export const createCompanySlice: StateCreator<
|
||||
|
||||
patchTask: (task) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
const idx = state.snapshot.tasks.findIndex((t) => t.id === task.id);
|
||||
if (!state.snapshotSections) return;
|
||||
const tasks = state.snapshotSections.company.tasks;
|
||||
const idx = tasks.findIndex((t) => t.id === task.id);
|
||||
if (idx >= 0) {
|
||||
state.snapshot.tasks[idx] = task;
|
||||
tasks[idx] = task;
|
||||
} else {
|
||||
state.snapshot.tasks.unshift(task);
|
||||
tasks.unshift(task);
|
||||
}
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
removeTask: (taskId) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
state.snapshot.tasks = state.snapshot.tasks.filter((t) => t.id !== taskId);
|
||||
if (!state.snapshotSections) return;
|
||||
state.snapshotSections.company.tasks = state.snapshotSections.company.tasks.filter(
|
||||
(t) => t.id !== taskId,
|
||||
);
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
|
||||
patchRun: (run) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
const idx = state.snapshot.runs.findIndex((r) => r.id === run.id);
|
||||
if (!state.snapshotSections) return;
|
||||
const runs = state.snapshotSections.company.runs;
|
||||
const idx = runs.findIndex((r) => r.id === run.id);
|
||||
if (idx >= 0) {
|
||||
state.snapshot.runs[idx] = run;
|
||||
runs[idx] = run;
|
||||
} else {
|
||||
state.snapshot.runs.unshift(run);
|
||||
runs.unshift(run);
|
||||
}
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
removeRun: (runId) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
state.snapshot.runs = state.snapshot.runs.filter((r) => r.id !== runId);
|
||||
if (!state.snapshotSections) return;
|
||||
state.snapshotSections.company.runs = state.snapshotSections.company.runs.filter(
|
||||
(r) => r.id !== runId,
|
||||
);
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
|
||||
patchAgent: (agent) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
const idx = state.snapshot.agents.findIndex((a) => a.id === agent.id);
|
||||
if (!state.snapshotSections) return;
|
||||
const agents = state.snapshotSections.company.agents;
|
||||
const idx = agents.findIndex((a) => a.id === agent.id);
|
||||
if (idx >= 0) {
|
||||
state.snapshot.agents[idx] = agent;
|
||||
agents[idx] = agent;
|
||||
} else {
|
||||
state.snapshot.agents.unshift(agent);
|
||||
agents.unshift(agent);
|
||||
}
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
removeAgent: (agentId) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
state.snapshot.agents = state.snapshot.agents.filter((a) => a.id !== agentId);
|
||||
if (!state.snapshotSections) return;
|
||||
state.snapshotSections.company.agents = state.snapshotSections.company.agents.filter(
|
||||
(a) => a.id !== agentId,
|
||||
);
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
|
||||
patchApproval: (approval) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
const idx = state.snapshot.approvals.findIndex((a) => a.id === approval.id);
|
||||
if (!state.snapshotSections) return;
|
||||
const approvals = state.snapshotSections.company.approvals;
|
||||
const idx = approvals.findIndex((a) => a.id === approval.id);
|
||||
if (idx >= 0) {
|
||||
state.snapshot.approvals[idx] = approval;
|
||||
approvals[idx] = approval;
|
||||
} else {
|
||||
state.snapshot.approvals.unshift(approval);
|
||||
approvals.unshift(approval);
|
||||
}
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
removeApproval: (approvalId) =>
|
||||
set((state) => {
|
||||
if (!state.snapshot) return;
|
||||
state.snapshot.approvals = state.snapshot.approvals.filter((a) => a.id !== approvalId);
|
||||
if (!state.snapshotSections) return;
|
||||
state.snapshotSections.company.approvals = state.snapshotSections.company.approvals.filter(
|
||||
(a) => a.id !== approvalId,
|
||||
);
|
||||
applySnapshotSections(state, state.snapshotSections);
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { ProfileSnapshot } from "@shared/types";
|
||||
|
||||
export interface ProfileMetadataSection {
|
||||
currentCompanyId: ProfileSnapshot["currentCompanyId"];
|
||||
profilePath: ProfileSnapshot["profilePath"];
|
||||
updatesEnabled: ProfileSnapshot["updatesEnabled"];
|
||||
theme: ProfileSnapshot["theme"];
|
||||
locale: ProfileSnapshot["locale"];
|
||||
backend: ProfileSnapshot["backend"];
|
||||
}
|
||||
|
||||
export interface ProfileCatalogSection {
|
||||
companies: ProfileSnapshot["companies"];
|
||||
connectors: ProfileSnapshot["connectors"];
|
||||
}
|
||||
|
||||
export interface CompanyCollectionsSection {
|
||||
workspaces: ProfileSnapshot["workspaces"];
|
||||
agents: ProfileSnapshot["agents"];
|
||||
goals: ProfileSnapshot["goals"];
|
||||
projects: ProfileSnapshot["projects"];
|
||||
tasks: ProfileSnapshot["tasks"];
|
||||
approvals: ProfileSnapshot["approvals"];
|
||||
runs: ProfileSnapshot["runs"];
|
||||
costs: ProfileSnapshot["costs"];
|
||||
activity: ProfileSnapshot["activity"];
|
||||
secrets: ProfileSnapshot["secrets"];
|
||||
comments: ProfileSnapshot["comments"];
|
||||
socialAccounts: ProfileSnapshot["socialAccounts"];
|
||||
meetings: ProfileSnapshot["meetings"];
|
||||
documents: ProfileSnapshot["documents"];
|
||||
workProducts: ProfileSnapshot["workProducts"];
|
||||
knowledgeBase: ProfileSnapshot["knowledgeBase"];
|
||||
sprints: ProfileSnapshot["sprints"];
|
||||
automationRules: ProfileSnapshot["automationRules"];
|
||||
agentMessages: ProfileSnapshot["agentMessages"];
|
||||
workflows: ProfileSnapshot["workflows"];
|
||||
}
|
||||
|
||||
export interface ProfileSnapshotSections {
|
||||
metadata: ProfileMetadataSection;
|
||||
catalog: ProfileCatalogSection;
|
||||
company: CompanyCollectionsSection;
|
||||
}
|
||||
|
||||
export function splitProfileSnapshot(snapshot: ProfileSnapshot): ProfileSnapshotSections {
|
||||
return {
|
||||
metadata: {
|
||||
currentCompanyId: snapshot.currentCompanyId,
|
||||
profilePath: snapshot.profilePath,
|
||||
updatesEnabled: snapshot.updatesEnabled,
|
||||
theme: snapshot.theme,
|
||||
locale: snapshot.locale,
|
||||
backend: snapshot.backend,
|
||||
},
|
||||
catalog: {
|
||||
companies: snapshot.companies,
|
||||
connectors: snapshot.connectors,
|
||||
},
|
||||
company: {
|
||||
workspaces: snapshot.workspaces,
|
||||
agents: snapshot.agents,
|
||||
goals: snapshot.goals,
|
||||
projects: snapshot.projects,
|
||||
tasks: snapshot.tasks,
|
||||
approvals: snapshot.approvals,
|
||||
runs: snapshot.runs,
|
||||
costs: snapshot.costs,
|
||||
activity: snapshot.activity,
|
||||
secrets: snapshot.secrets,
|
||||
comments: snapshot.comments,
|
||||
socialAccounts: snapshot.socialAccounts,
|
||||
meetings: snapshot.meetings,
|
||||
documents: snapshot.documents,
|
||||
workProducts: snapshot.workProducts,
|
||||
knowledgeBase: snapshot.knowledgeBase,
|
||||
sprints: snapshot.sprints,
|
||||
automationRules: snapshot.automationRules,
|
||||
agentMessages: snapshot.agentMessages,
|
||||
workflows: snapshot.workflows,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function assembleProfileSnapshot(sections: ProfileSnapshotSections): ProfileSnapshot {
|
||||
return {
|
||||
currentCompanyId: sections.metadata.currentCompanyId,
|
||||
profilePath: sections.metadata.profilePath,
|
||||
updatesEnabled: sections.metadata.updatesEnabled,
|
||||
theme: sections.metadata.theme,
|
||||
locale: sections.metadata.locale,
|
||||
backend: sections.metadata.backend,
|
||||
companies: sections.catalog.companies,
|
||||
connectors: sections.catalog.connectors,
|
||||
workspaces: sections.company.workspaces,
|
||||
agents: sections.company.agents,
|
||||
goals: sections.company.goals,
|
||||
projects: sections.company.projects,
|
||||
tasks: sections.company.tasks,
|
||||
approvals: sections.company.approvals,
|
||||
runs: sections.company.runs,
|
||||
costs: sections.company.costs,
|
||||
activity: sections.company.activity,
|
||||
secrets: sections.company.secrets,
|
||||
comments: sections.company.comments,
|
||||
socialAccounts: sections.company.socialAccounts,
|
||||
meetings: sections.company.meetings,
|
||||
documents: sections.company.documents,
|
||||
workProducts: sections.company.workProducts,
|
||||
knowledgeBase: sections.company.knowledgeBase,
|
||||
sprints: sections.company.sprints,
|
||||
automationRules: sections.company.automationRules,
|
||||
agentMessages: sections.company.agentMessages,
|
||||
workflows: sections.company.workflows,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user