From 8558acbbc98e078a0325606f5816ad1ec24b1955 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 5 May 2026 10:42:13 +0300 Subject: [PATCH] fix(landing): publish docs with pages build --- .github/workflows/landing.yml | 2 +- landing/nuxt.config.ts | 5 +- .../hooks/useComposerDraft.lifecycle.test.tsx | 57 ++++++++++++----- src/renderer/hooks/useComposerDraft.ts | 8 +-- src/renderer/services/composerDraftStorage.ts | 61 ++++++++++++++++++- test/renderer/hooks/useComposerDraft.test.ts | 57 +++++++++++++++++ 6 files changed, 168 insertions(+), 22 deletions(-) diff --git a/.github/workflows/landing.yml b/.github/workflows/landing.yml index c57c16f4..777604bc 100644 --- a/.github/workflows/landing.yml +++ b/.github/workflows/landing.yml @@ -35,7 +35,7 @@ jobs: NUXT_APP_BASE_URL: /claude_agent_teams_ui/ NUXT_PUBLIC_SITE_URL: https://777genius.github.io/claude_agent_teams_ui NUXT_PUBLIC_GITHUB_REPO: 777genius/claude_agent_teams_ui - run: npx nuxt generate + run: npm run generate:all - uses: actions/configure-pages@v5 diff --git a/landing/nuxt.config.ts b/landing/nuxt.config.ts index 240275d0..ae995584 100644 --- a/landing/nuxt.config.ts +++ b/landing/nuxt.config.ts @@ -8,6 +8,7 @@ const siteUrl = process.env.NUXT_PUBLIC_SITE_URL || "https://777genius.github.io const githubRepo = process.env.NUXT_PUBLIC_GITHUB_REPO || "777genius/claude_agent_teams_ui"; const githubReleasesUrl = `https://github.com/${githubRepo}/releases`; const baseURL = process.env.NUXT_APP_BASE_URL || "/"; +const basePrefixedDocsPath = `${baseURL.replace(/\/?$/, "/")}docs`; export default defineNuxtConfig({ compatibilityDate: "2026-01-19", @@ -57,7 +58,9 @@ export default defineNuxtConfig({ prerender: { ignore: [ "/docs", - "/docs/**" + "/docs/**", + basePrefixedDocsPath, + `${basePrefixedDocsPath}/**` ], routes: [ ...generateI18nRoutes(), diff --git a/src/renderer/hooks/useComposerDraft.lifecycle.test.tsx b/src/renderer/hooks/useComposerDraft.lifecycle.test.tsx index b2f40352..1bccc1fb 100644 --- a/src/renderer/hooks/useComposerDraft.lifecycle.test.tsx +++ b/src/renderer/hooks/useComposerDraft.lifecycle.test.tsx @@ -4,29 +4,55 @@ import { createRoot } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const { + deleteSnapshotIfMatchesMock, deleteSnapshotMock, emptySnapshotMock, loadSnapshotMock, migrateLegacyMock, saveSnapshotMock, -} = vi.hoisted(() => ({ - deleteSnapshotMock: vi.fn(), - emptySnapshotMock: vi.fn((teamName: string) => ({ - version: 1, - teamName, - text: '', - chips: [], - attachments: [], - actionMode: 'do', - updatedAt: Date.now(), - })), - loadSnapshotMock: vi.fn(), - migrateLegacyMock: vi.fn(), - saveSnapshotMock: vi.fn(), -})); +} = vi.hoisted(() => { + interface MockSnapshot { + version: number; + teamName: string; + text: string; + chips: unknown[]; + attachments: unknown[]; + actionMode?: string; + pendingSendId?: string; + updatedAt: number; + } + + const deleteSnapshot = vi.fn(); + const loadSnapshot = vi.fn(); + + return { + deleteSnapshotIfMatchesMock: vi.fn( + async (teamName: string, predicate: (snapshot: MockSnapshot | null) => boolean) => { + const snapshot = (await loadSnapshot(teamName)) as MockSnapshot | null; + if (predicate(snapshot)) { + await deleteSnapshot(teamName); + } + } + ), + deleteSnapshotMock: deleteSnapshot, + emptySnapshotMock: vi.fn((teamName: string) => ({ + version: 1, + teamName, + text: '', + chips: [], + attachments: [], + actionMode: 'do', + updatedAt: Date.now(), + })), + loadSnapshotMock: loadSnapshot, + migrateLegacyMock: vi.fn(), + saveSnapshotMock: vi.fn(), + }; +}); vi.mock('@renderer/services/composerDraftStorage', () => ({ composerDraftStorage: { + deleteSnapshotIfMatches: deleteSnapshotIfMatchesMock, deleteSnapshot: deleteSnapshotMock, emptySnapshot: emptySnapshotMock, loadSnapshot: loadSnapshotMock, @@ -90,6 +116,7 @@ async function renderLoadedHook(): Promise<{ describe('useComposerDraft pending send lifecycle', () => { beforeEach(() => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + deleteSnapshotIfMatchesMock.mockClear(); deleteSnapshotMock.mockReset(); emptySnapshotMock.mockClear(); loadSnapshotMock.mockReset(); diff --git a/src/renderer/hooks/useComposerDraft.ts b/src/renderer/hooks/useComposerDraft.ts index dc6a76ae..18df8931 100644 --- a/src/renderer/hooks/useComposerDraft.ts +++ b/src/renderer/hooks/useComposerDraft.ts @@ -571,10 +571,10 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult { (teamNameForDelete: string, submittedContent?: ComposerDraftContent) => enqueuePersist(async () => { if (submittedContent != null) { - const currentSnapshot = await composerDraftStorage.loadSnapshot(teamNameForDelete); - if (!snapshotMatchesContent(currentSnapshot, submittedContent)) { - return; - } + await composerDraftStorage.deleteSnapshotIfMatches(teamNameForDelete, (snapshot) => + snapshotMatchesContent(snapshot, submittedContent) + ); + return; } await composerDraftStorage.deleteSnapshot(teamNameForDelete); }), diff --git a/src/renderer/services/composerDraftStorage.ts b/src/renderer/services/composerDraftStorage.ts index 43151171..0ca5c8ab 100644 --- a/src/renderer/services/composerDraftStorage.ts +++ b/src/renderer/services/composerDraftStorage.ts @@ -6,7 +6,7 @@ * Drafts persist until explicitly cleared (on send or manual action). */ -import { del, get, set } from 'idb-keyval'; +import { createStore, del, get, promisifyRequest, set } from 'idb-keyval'; import type { InlineChip } from '@renderer/types/inlineChip'; import type { AgentActionMode, AttachmentPayload } from '@shared/types'; @@ -34,6 +34,7 @@ export interface ComposerDraftSnapshot { // --------------------------------------------------------------------------- const KEY_PREFIX = 'composer:'; +const keyvalStore = createStore('keyval-store', 'keyval'); function storageKey(teamName: string): string { return `${KEY_PREFIX}${teamName}`; @@ -83,6 +84,43 @@ function markIdbUnavailable(): void { idbUnavailable = true; } +function toError(error: unknown, fallbackMessage: string): Error { + return error instanceof Error ? error : new Error(fallbackMessage); +} + +function deleteMatchingSnapshotInStore( + store: IDBObjectStore, + key: string, + predicate: (snapshot: ComposerDraftSnapshot | null) => boolean +): Promise { + return new Promise((resolve, reject) => { + const request = store.get(key); + + request.onerror = () => { + reject(toError(request.error, 'Failed to read composer draft snapshot.')); + }; + + request.onsuccess = () => { + try { + const raw = request.result as unknown; + const snapshot = raw == null ? null : isValidSnapshot(raw) ? raw : null; + const shouldDelete = (raw != null && snapshot == null) || predicate(snapshot); + + if (shouldDelete) { + store.delete(key); + } + + void promisifyRequest(store.transaction).then( + () => resolve(), + (error) => reject(toError(error, 'Failed to delete composer draft snapshot.')) + ); + } catch (error) { + reject(toError(error, 'Failed to evaluate composer draft snapshot.')); + } + }; + }); +} + // --------------------------------------------------------------------------- // Core API // --------------------------------------------------------------------------- @@ -133,6 +171,26 @@ async function deleteSnapshot(teamName: string): Promise { } } +async function deleteSnapshotIfMatches( + teamName: string, + predicate: (snapshot: ComposerDraftSnapshot | null) => boolean +): Promise { + const key = storageKey(teamName); + if (idbUnavailable) { + const snapshot = fallbackStore.get(key) ?? null; + if (predicate(snapshot)) fallbackStore.delete(key); + return; + } + + try { + await keyvalStore('readwrite', (store) => deleteMatchingSnapshotInStore(store, key, predicate)); + } catch { + markIdbUnavailable(); + const snapshot = fallbackStore.get(key) ?? null; + if (predicate(snapshot)) fallbackStore.delete(key); + } +} + // --------------------------------------------------------------------------- // Legacy migration // --------------------------------------------------------------------------- @@ -267,6 +325,7 @@ export const composerDraftStorage = { saveSnapshot, loadSnapshot, deleteSnapshot, + deleteSnapshotIfMatches, migrateLegacy, emptySnapshot, }; diff --git a/test/renderer/hooks/useComposerDraft.test.ts b/test/renderer/hooks/useComposerDraft.test.ts index 20592bf6..0e7ecbde 100644 --- a/test/renderer/hooks/useComposerDraft.test.ts +++ b/test/renderer/hooks/useComposerDraft.test.ts @@ -4,6 +4,35 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const store = new Map(); vi.mock('idb-keyval', () => ({ + createStore: vi.fn( + () => + async ( + _txMode: IDBTransactionMode, + callback: (objectStore: IDBObjectStore) => T | PromiseLike + ): Promise => { + const objectStore = { + transaction: {}, + get: (key: string) => { + const request = { + result: store.get(key) ?? undefined, + error: null, + onsuccess: null as (() => void) | null, + onerror: null as (() => void) | null, + }; + queueMicrotask(() => { + request.onsuccess?.(); + }); + return request; + }, + delete: (key: string) => { + store.delete(key); + }, + }; + + return callback(objectStore as unknown as IDBObjectStore); + } + ), + promisifyRequest: vi.fn(() => Promise.resolve(undefined)), get: vi.fn((key: string) => Promise.resolve(store.get(key) ?? undefined)), set: vi.fn((key: string, value: unknown) => { store.set(key, value); @@ -91,6 +120,34 @@ describe('composerDraftStorage', () => { }); }); + describe('deleteSnapshotIfMatches', () => { + it('should delete a snapshot when the predicate matches', async () => { + await composerDraftStorage.saveSnapshot( + 'team-a', + makeSnapshot('team-a', { pendingSendId: 'send-1' }) + ); + + await composerDraftStorage.deleteSnapshotIfMatches( + 'team-a', + (snapshot) => snapshot?.pendingSendId === 'send-1' + ); + + expect(await composerDraftStorage.loadSnapshot('team-a')).toBeNull(); + }); + + it('should keep a snapshot when the predicate does not match', async () => { + const snap = makeSnapshot('team-a', { pendingSendId: 'newer-send' }); + await composerDraftStorage.saveSnapshot('team-a', snap); + + await composerDraftStorage.deleteSnapshotIfMatches( + 'team-a', + (snapshot) => snapshot?.pendingSendId === 'older-send' + ); + + expect(await composerDraftStorage.loadSnapshot('team-a')).toEqual(snap); + }); + }); + describe('team isolation', () => { it('should isolate drafts by teamName', async () => { const snapA = makeSnapshot('team-a', { text: 'from team A' });