feat(web): Integrate M4-LLM error handling improvements
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/pr/woodpecker Pipeline failed

Port high-value features from work/m4-llm branch into develop's
security-hardened codebase:

- Separate LLM vs persistence error handling in useChat (shows
  assistant response even when save fails)
- Add structured error context logging with errorType, messagePreview,
  messageCount fields for debugging
- Enforce state invariant in useChatOverlay: cannot be minimized when
  closed
- Add onStorageError callback with user-friendly messages and
  per-error-type deduplication
- Add error logging to Chat imperative handle methods
- Create Chat.test.tsx with loadConversation failure mode tests

Skipped from work/m4-llm (superseded by develop):
- AbortSignal timeout (develop has centralized client timeout)
- Custom toast system (duplicates @mosaic/ui)
- ErrorBoundary (develop has its own)
- WebSocket typed events (develop's ref-based pattern is superior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-06 20:04:53 -06:00
parent ac796072d8
commit 893a139087
8 changed files with 598 additions and 17 deletions

View File

@@ -170,7 +170,12 @@ describe("isMessageArray", () => {
describe("isChatOverlayState", () => {
it("should return true for a valid ChatOverlayState", () => {
expect(isChatOverlayState({ isOpen: true, isMinimized: false })).toBe(true);
expect(isChatOverlayState({ isOpen: false, isMinimized: true })).toBe(true);
expect(isChatOverlayState({ isOpen: true, isMinimized: true })).toBe(true);
expect(isChatOverlayState({ isOpen: false, isMinimized: false })).toBe(true);
});
it("should reject invalid state: closed AND minimized (invariant violation)", () => {
expect(isChatOverlayState({ isOpen: false, isMinimized: true })).toBe(false);
});
it("should return false for non-object values", () => {

View File

@@ -76,12 +76,16 @@ export function isMessageArray(value: unknown): value is {
/**
* Type guard: validates ChatOverlayState shape
* Expects { isOpen: boolean, isMinimized: boolean }
* Enforces invariant: cannot be minimized when closed
*/
export function isChatOverlayState(
value: unknown
): value is { isOpen: boolean; isMinimized: boolean } {
if (!isRecord(value)) return false;
return typeof value.isOpen === "boolean" && typeof value.isMinimized === "boolean";
if (typeof value.isOpen !== "boolean" || typeof value.isMinimized !== "boolean") return false;
// Invariant: cannot be minimized when closed
if (!value.isOpen && value.isMinimized) return false;
return true;
}
/**