merge(dev): sync dev into main
This commit is contained in:
commit
5f051cc6f3
6 changed files with 168 additions and 22 deletions
2
.github/workflows/landing.yml
vendored
2
.github/workflows/landing.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
return new Promise<void>((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<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteSnapshotIfMatches(
|
||||
teamName: string,
|
||||
predicate: (snapshot: ComposerDraftSnapshot | null) => boolean
|
||||
): Promise<void> {
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,35 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|||
const store = new Map<string, unknown>();
|
||||
|
||||
vi.mock('idb-keyval', () => ({
|
||||
createStore: vi.fn(
|
||||
() =>
|
||||
async <T>(
|
||||
_txMode: IDBTransactionMode,
|
||||
callback: (objectStore: IDBObjectStore) => T | PromiseLike<T>
|
||||
): Promise<T> => {
|
||||
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' });
|
||||
|
|
|
|||
Loading…
Reference in a new issue