Files
fusion/packages/dashboard/app/hooks/useToast.ts
gsxdsm 478f226a54 test: green full-suite CI after main drift (#2229)
## Summary
Restores green **Full Suite (non-blocking)** runs on `main`. Recent main
merges left i18n key parity, schema baseline bookkeeping (0011→0012),
heartbeat tool inventory (FN-8058 `fn_task_logs_read`), and merger
whitespace-classification mocks (execFile `git diff -p -w :2: :3:`) out
of date, so all four test shards failed.

## Root causes observed on main
- **Shard 4 / `@fusion/i18n`**: missing `skipConfirmationDialogs*` +
`reviewBudgetExhausted` in non-en locales; orphan
`awaitingApprovalPlanReviewReplanCap`
- **Shard 3 / `@fusion/core`**: `SCHEMA_BASELINE_VERSION` advanced to
`0012` while tests still equated it with
`OWNER_PROJECT_ID_SPLIT_VERSION` (`0011`) and omitted `0012` from
applied-migration lists
- **Shards 1–2 / `@fusion/engine`**: tool count/snapshot drift for
`fn_task_logs_read`; merger tests still mocked `git diff-tree` for
trivial classification after the execFile `:2:`/`:3:` cutover; mock
provider `updateTask` arity drift

## Changes
- Locale catalogs: add missing keys, drop orphan key
- Schema applier tests: immutable 0011 identity + baseline 0012 lists
- Heartbeat + gating snapshots: include `fn_task_logs_read`
- Merger unit mocks: recognize `git diff -p -w :2:path :3:path`
- Mock provider: accept optional third `updateTask` arg

## Test plan
- [x] `pnpm --filter @fusion/i18n exec vitest run` — 23/23
- [x] `pnpm --filter @fusion/core exec vitest run
src/__tests__/postgres/schema-applier.test.ts` (immutable + automation
upgrade) — pass
- [x] `pnpm --filter @fusion/core exec vitest run` project-identity +
satellite-fusiondir — pass
- [x] Engine suites from failed CI shards (file-scoped,
hermes/openclaw/paperclip/grok, reliability post-finalize/mission,
heartbeat, gating, merger recovery/prompt, mock-provider, etc.) — pass
- [ ] Full Suite workflow green on merge to main

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Improved project data isolation across backend operations.
- Added safer optional toast handling when UI components render outside
the full application shell.
  - Added support for reading task logs during agent heartbeat sessions.

- **Bug Fixes**
- Prevented runtime probes from hanging and avoided scanning large
binary files.
  - Improved path handling for workspaces with missing descendants.
- Corrected task retry state resets and GitHub import/issue-close
behavior.

- **Style**
  - Improved chat, terminal, and settings spacing.
  - Added clearer accessibility labeling for the auto-merge control.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 16:01:29 -07:00

72 lines
2.5 KiB
TypeScript

import { useState, useCallback, useRef, createContext, useContext, useEffect } from "react";
import type { ReactNode } from "react";
import React from "react";
export type ToastType = "success" | "error" | "info" | "warning";
export interface Toast {
id: number;
message: string;
type: ToastType;
}
interface ToastContextValue {
toasts: Toast[];
addToast: (message: string, type?: ToastType) => void;
removeToast: (id: number) => void;
}
const ToastContext = createContext<ToastContextValue | null>(null);
export function ToastProvider({ children }: { children: ReactNode }) {
const [toasts, setToasts] = useState<Toast[]>([]);
const nextId = useRef(0);
const timers = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map());
const removeToast = useCallback((id: number) => {
const timer = timers.current.get(id);
if (timer !== undefined) {
clearTimeout(timer);
timers.current.delete(id);
}
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const addToast = useCallback((message: string, type: ToastType = "info") => {
// Suppress bare network-failure toasts (TypeError "Failed to fetch" from
// aborted in-flight requests when the tab is backgrounded/resumed on
// mobile). Toasts with additional context still pass through.
if (type === "error" && /^\s*Failed to fetch\.?\s*$/i.test(message)) {
return;
}
const id = nextId.current++;
setToasts((prev) => [...prev, { id, message, type }]);
const timer = setTimeout(() => removeToast(id), 4000);
timers.current.set(id, timer);
}, [removeToast]);
// Clear all pending timers on unmount to prevent state updates on unmounted components
useEffect(() => {
return () => {
timers.current.forEach((timer) => clearTimeout(timer));
timers.current.clear();
};
}, []);
return React.createElement(ToastContext.Provider, { value: { toasts, addToast, removeToast } }, children);
}
export function useToast(): ToastContextValue {
const ctx = useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within ToastProvider");
return ctx;
}
/*
FNXC:ToastProvider 2026-07-14-19:25:
Board-card decorations such as RuntimeFallbackBadge may mount outside a full app shell (unit/harness tests and optional plugin surfaces). Optional toast access lets those leaves skip toasting instead of throwing and blanking the board via ErrorBoundary.
*/
export function useOptionalToast(): ToastContextValue | null {
return useContext(ToastContext);
}