import type { AiCfoSuggestedQuestionsPageContext } from '../../common/aiCfo/aiCfoSuggestedQuestionsPageContext'; import { FetchStateAndError, ID } from '../../commonStateTypes/common'; import { PageToken } from '../../commonStateTypes/viewAndReport/viewAndReport'; import { ContextStatus } from './aiCfoViewPayload'; export interface PageTokenState { hasMore: boolean; nextPageToken: PageToken; } export interface AiCfoViewUIState { chatHistoryNextPageToken: { [chatSessionId: ID]: PageTokenState; }; /** Last focused chat session id per side-panel host page key (e.g. `dashboard`, `reports:profit_loss`). */ chatSessionIdByPageKey: Partial>; chatSessionsFirstPageFetched: boolean; chatSessionsNextPageToken: PageTokenState; cotCollapsedByQuestionAnswerId: { [questionAnswerId: ID]: boolean; }; currentInput: string; scrollPositionByChatSessionId: { [chatSessionId: ID]: { scrollTop: number; }; }; agentId?: ID; /** * When set, the side panel on host routes (e.g. dashboard/reports) is scoped to this **page key**; * session ids are mirrored into {@link chatSessionIdByPageKey}. Full `/ai-cfo/...` should clear this. */ aiCfoSidePanelHostPageKey?: string; currentChatSessionId?: ID; /** * Whether the AI CFO page body shows the Skills browser instead of the chat. * * In Redux rather than component state because the entry that opens it lives in the * app drawer's rail (`AiCfoMenuContent`, reached via both `AppDrawer` on desktop and * the sidebar drawer on mobile) while the browser renders in `AiCfoPage` — sibling * subtrees with no common owner below the store. */ /** * Whether the AI CFO page body shows the Routines list instead of the chat. * * Lives here, not in component state, for the same reason as the Skills flag: * the entries that open it are in the app drawer and the mobile sidebar, * neither of which is an ancestor of the screen that renders it. */ isRoutinesBrowserOpen?: boolean; isSkillsBrowserOpen?: boolean; lastContextMessage?: string; lastContextStatus?: ContextStatus; } export interface SuggestedQuestionsForPageContext extends FetchStateAndError { pageContext: AiCfoSuggestedQuestionsPageContext; suggestedQuestions: string[]; generatedAt?: string; } /** A chat-visible skill shown in the `/` menu. */ export interface Skill { description: string | null; displayName: string | null; /** * Display-ready example phrases for the `/` menu's "try: …" line, flattened from * the macro's parameter_hints. Empty when the macro declares no hints. */ exampleHints: string[]; hasScripts: boolean; /** * Whether the signed-in user authored this skill. Distinct from `isOwnedByTenant`, and * the stricter of the two: writes require ownership, so a colleague's shared skill is * `isOwnedByTenant: true` and `isMine: false`. False for a Zeni default. */ isMine: boolean; /** False for a Zeni default, true for a skill this tenant owns. */ isOwnedByTenant: boolean; macroId: string; name: string; semver: string | null; /** ISO timestamp of the latest version, for the list's "last updated" column. */ updatedAt: string | null; /** * 'private' (author-only) or 'tenant' (everyone). Null for Zeni defaults, where it does * not apply — and also null for a value this client does not recognise, which is * deliberate: consumers render no audience badge rather than a wrong one, and adding a * third tier server-side then surfaces here as a code change instead of silently * matching none of the branches. * * The same union as {@link SkillDraft.visibility}. It was `string | null`, which left * every consumer comparing against bare literals with no compile-time backing on the * READ path while the write path had it. */ visibility: SkillVisibility | null; } /** One skill's prompt plus the rendered card, from GET /1.0/skills/. */ export interface SkillDetail { artifact: string; artifactTitle: string; instructions: string; instructionsCharCount: number; skill: Skill; } /** How often a routine runs. `once` exists server-side but is not offered. */ export type RoutineRecurrence = 'daily' | 'weekly' | 'monthly'; /** The outcome of a routine's last run, as the list badges it. */ export type RoutineStatus = 'succeeded' | 'failed' | 'timeout'; /** * A scheduled recurring run, from GET /1.0/routines. * * A routine is a schedule pointing at a skill — which is why `macroId` is always * present even for one the user wrote as a bare prompt. In that case `isAdHoc` * is true and the skill exists only behind this routine: it is not in the `/` * menu, and the list offers "save as a skill" rather than "open the skill". */ export interface Routine { consecutiveFailures: number; dayOfMonth: number | null; /** False when paused by the user, or auto-paused after repeated failures. */ enabled: boolean; hour: number; isAdHoc: boolean; lastRunAt: string | null; lastStatus: RoutineStatus | null; macroId: ID; minute: number; name: string; nextRunAt: string | null; /** * The skill version this routine actually runs. * * Pinned deliberately: editing a skill must never silently change what goes * out unattended, so moving a routine forward is an explicit act. Null only * for a routine that predates pinning. */ pinnedVersion: number | null; /** Null when this client does not recognise the server's cadence. */ recurrence: RoutineRecurrence | null; scheduleId: ID; /** The one conversation this routine appends to, run after run. */ sessionId: string | null; /** Null only if the server omitted it, which means something is wrong. */ tzName: string | null; /** Runs whose answer has not been opened. Drives the "N new" badge. */ unreadRuns: number; weekday: number | null; } /** * What the routine form submits. * * Exactly one of `macroId` and `prompt`: scheduling an existing skill, or a * one-off the user typed. Both, or neither, is refused by the server. */ export interface RoutineDraft { hour: number; name: string; recurrence: RoutineRecurrence; tzName: string; dayOfMonth?: number; enabled?: boolean; macroId?: ID; minute?: number; prompt?: string; weekday?: number; } /** * In-flight state for a routine WRITE, kept apart from the list. * * The list and the mutations shared one `fetchState`/`error` pair, and every * write refetches — so a failed save or delete had its error erased by its own * refresh, in the same tick. For a feature that emails financial reports on a * schedule, "we told you it saved and it did not" is the worst thing this can * do. The skills slice keeps `skillMutation` separate for exactly this reason. */ export interface RoutineMutationState extends FetchStateAndError { /** WHICH write this is reporting, so a delete cannot drive a save's button. */ kind: 'save' | 'delete' | 'run' | undefined; /** * The routine a Run-now was requested for. * * Held because the list shows the feedback on that ROW, and `kind` alone * cannot say which one — unlike save and delete, which drive a single form or * a single confirm. */ runningScheduleId?: ID; } /** The outcome of one run, as the history badges it. */ export type RoutineRunStatus = 'claimed' | 'running' | 'succeeded' | 'failed' | 'timeout'; export interface RoutineRun { attempt: number; /** Why it failed. The only place a failed run explains itself. */ error: string | null; finishedAt: string | null; /** Started by someone pressing Run, rather than by the schedule. */ isManual: boolean; /** * The message this run produced, inside the routine's one conversation. * * Null when the agent's response did not name one — the run then opens the * thread rather than a message that may not exist. */ questionAnswerId: ID | null; runId: ID; scheduledFor: string | null; /** This run's own conversation. Null on runs from before runs had one. */ sessionId: ID | null; startedAt: string | null; /** Null when this client does not recognise the server's status. */ status: RoutineRunStatus | null; } /** * The run history for ONE routine — whichever the user has expanded. * * A single slot rather than a map keyed by routine: only one history is open at * a time, so a map would be state to invalidate for no behaviour anyone can * see. */ export interface RoutineRunsState extends FetchStateAndError { runs: RoutineRun[]; /** Which routine these belong to, so a stale response cannot fill the wrong list. */ scheduleId: ID | null; /** The conversation every run of this routine appends to. */ sessionId: ID | null; } export interface RoutinesState extends FetchStateAndError { /** * Routines deleted locally, so a list response already in flight cannot put * them back. Same race, and the same reasoning, as `deletedMacroIds` on the * skills slice: a save refetches, and a delete can land while that is * outstanding. */ deletedScheduleIds: ID[]; /** * The server's per-user cap, so the client can disable "New routine" with a * reason rather than letting the user fill in a form and fail on save. */ maxRoutines: number; routines: Routine[]; /** * Whether the feature is on for this tenant, from the server. * * Starts false so the Routines entry stays hidden until the server says * otherwise — an empty `routines` list is not evidence either way, and a * tenant without the feature would otherwise see a dead entry. */ routinesEnabled: boolean; } export interface SkillsState extends FetchStateAndError { /** * Whether the signed-in user may publish a skill tenant-wide. From the server, which * is the only place that knows: it is a scope granted to admin roles. Starts false so * the control is withheld until the server grants it. */ canPublish: boolean; /** * Skills deleted locally, so a list response that was already in flight cannot put * them back. * * The delete and the refetch race by construction — a save emits `fetchSkills`, and a * delete can land while that is outstanding — and `fetchSkillsSuccess` replaces the * array wholesale. Without this the row reappears, which reads as the delete having * failed. Entries are dropped once a response arrives that no longer contains them, * so this cannot grow without bound or outlive the server agreeing. */ deletedMacroIds: string[]; skills: Skill[]; /** * Whether the feature is on for this tenant. Starts false so the Skills entry * stays hidden until the server says otherwise — an empty `skills` list is not * evidence either way. */ skillsEnabled: boolean; } /** * What the editor submits. Mirrors the server's write model, which is deliberately * narrower than a skill: `scripts`, `tenantId`, `macroId` and menu-visibility are all * rejected there, so they have no place here either. */ /** Who can see a skill. 'private' is its author only, 'tenant' is everyone. */ export type SkillVisibility = 'private' | 'tenant'; export interface SkillDraft { /** One line under the name in the menu. What makes a list browsable. */ description: string | null; /** The prompt. Multi-paragraph — this is the skill, functionally. */ instructions: string; /** The `/` command and the menu row's title. */ name: string; /** * The audience to SET, or null to leave it exactly as it is. * * Required, and null is a real choice rather than an oversight, because the two are * not interchangeable on the wire: the server treats an omitted visibility as * "unchanged" and an explicit 'private' as "make this private". A caller who cannot * publish must therefore send null — sending 'private' would unpublish a tenant-wide * skill on every unrelated edit, silently and with a 200. */ visibility: SkillVisibility | null; } /** One row of a skill's history. Metadata only — bodies are read per version. */ export interface SkillVersion { /** Menu name AT this version, so a rename is visible in the list. */ displayName: string | null; instructionsCharCount: number; isActive: boolean; /** Whether this is the version the `/` menu runs. */ isLatest: boolean; semver: string | null; updatedAt: string | null; version: number; } /** * One historical version's prompt, fetched on demand for a restore. * * Separate from `skillDetail`, which holds the LIVE version: a restore needs both at once * — the old body to seed the form, the current skill for its name and id — so writing the * pinned body into `skillDetail` would lose the thing being restored onto. */ export interface SkillVersionBodyState extends FetchStateAndError { instructions: string | undefined; /** Which (skill, version) `instructions` belongs to, so a stale response is dropped. */ macroId: string | undefined; version: number | undefined; } export interface SkillVersionsState extends FetchStateAndError { /** Which skill `versions` belongs to, so a stale response is never rendered. */ macroId: string | undefined; versions: SkillVersion[]; } /** * In-flight state for authoring. Separate from `skillDetail` because a save keeps the * skill on screen while it runs — the form needs to disable itself and show the error * without the prompt it is editing disappearing underneath. */ export interface SkillMutationState extends FetchStateAndError { /** * WHICH mutation this slice is reporting. Save, delete and visibility all share * it — safe while both consumers lived inside the Skills browser, which renders * one at a time. A chat draft card stays mounted independently, so without this * an unrelated delete drives its Save control through "Saving…" to "Saved". */ kind: 'save' | 'delete' | 'visibility' | undefined; /** * The skill the last successful save produced, so the caller can select it without * refetching the list. Undefined until a save succeeds. */ savedMacroId: string | undefined; } export interface SkillDetailState extends FetchStateAndError { detail: SkillDetail | undefined; /** Which skill `detail` belongs to, so a stale response is never rendered. */ macroId: string | undefined; } export interface AiCfoViewState extends FetchStateAndError { allChatSessionIds: ID[]; createSessionAndSubmitState: FetchStateAndError; createSessionState: FetchStateAndError; deleteChatSessionStateByChatSessionId: Record; fetchChatSessionHistoryByChatSessionId: Record; masterTOSAcceptanceState: FetchStateAndError; responseStateByChatSessionId: Record; /** The outcome of the last routine write, safe from list refreshes. */ routineMutation: RoutineMutationState; routineRuns: RoutineRunsState; /** Scheduled recurring runs — the Routines tab. */ routines: RoutinesState; /** The skill whose prompt is open in the Skills browser. */ skillDetail: SkillDetailState; /** Create / save-new-version / delete, while one is in flight. */ skillMutation: SkillMutationState; /** Chat-visible skills for the `/` menu and the Skills browser. */ skills: SkillsState; /** A historical version's prompt, while a restore is being set up. */ skillVersionBody: SkillVersionBodyState; /** Version history for the skill open in the browser. */ skillVersions: SkillVersionsState; stopSubmitStateByChatSessionId: Record; /** Keyed by `page_context` query value (e.g. balance_sheet). */ suggestedQuestionsByPageContext: Partial>; uiState: AiCfoViewUIState; }