diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6259f937a9..afd8d0540b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -549,7 +549,7 @@ jobs: name: Frontend Build runs-on: ubuntu-latest env: - NODE_OPTIONS: --max-old-space-size=6144 + NODE_OPTIONS: --max-old-space-size=6144 --no-experimental-webstorage needs: build-impact steps: - uses: actions/checkout@v5 diff --git a/src/crates/assembly/core/src/service_agent_runtime.rs b/src/crates/assembly/core/src/service_agent_runtime.rs index 541686d430..2ca4966ebd 100644 --- a/src/crates/assembly/core/src/service_agent_runtime.rs +++ b/src/crates/assembly/core/src/service_agent_runtime.rs @@ -2924,7 +2924,12 @@ impl CoreRemoteSessionRuntimeHost { pub(crate) fn new() -> Result { let coordinator = get_global_coordinator() .ok_or_else(|| "Desktop session system not ready".to_string())?; - let runtime = CoreServiceAgentRuntime::agent_runtime(coordinator.clone())?; + let scheduler = get_global_scheduler() + .ok_or_else(|| "Dialog scheduler is not initialized".to_string())?; + let runtime = CoreServiceAgentRuntime::agent_runtime_with_dialog_turns( + coordinator.clone(), + scheduler, + )?; Ok(Self { coordinator, runtime, @@ -4125,6 +4130,30 @@ mod tests { .expect("remote session rollback"); assert!(rollback.contains("ensure_remote_binding_runtime_ownership")); assert!(rollback.contains("binding.is_remote()")); + + let remote_session_host_impl = source + .split("impl CoreRemoteSessionRuntimeHost") + .nth(1) + .and_then(|source| source.split("struct CoreRemotePollRuntimeHost").next()) + .expect("remote session host struct implementation"); + assert!( + remote_session_host_impl.contains("agent_runtime_with_dialog_turns"), + "CoreRemoteSessionRuntimeHost must register dialogue and revert ports for rollback" + ); + + let dialog_turns_builder = source + .split("pub(crate) fn agent_runtime_with_dialog_turns") + .nth(1) + .and_then(|source| { + source + .split("pub(crate) fn agent_runtime_with_lifecycle_delivery") + .next() + }) + .expect("agent_runtime_with_dialog_turns definition"); + assert!( + dialog_turns_builder.contains(".with_session_revert_port(session_revert)"), + "agent_runtime_with_dialog_turns must attach session_revert port" + ); } #[test] @@ -4628,8 +4657,13 @@ mod history_workspace_identity_tests { async fn history_routing_uses_ids_even_with_colliding_roots_and_stale_transport_fields() { let temp = tempfile::tempdir().unwrap(); let local = register_local_fixture(temp.path(), None).await; + let remote_path = if local.root_path.to_string_lossy().starts_with('/') { + local.root_path.to_string_lossy().to_string() + } else { + format!("/srv/colliding-test/{}", uuid::Uuid::new_v4()) + }; let remote = register_remote_fixture( - &local.root_path.to_string_lossy(), + &remote_path, "history-test-ssh", "history.example", ) diff --git a/src/mobile-web/src/components/ChatMessageActions.tsx b/src/mobile-web/src/components/ChatMessageActions.tsx index faa8b9cd59..2b004a61bd 100644 --- a/src/mobile-web/src/components/ChatMessageActions.tsx +++ b/src/mobile-web/src/components/ChatMessageActions.tsx @@ -36,11 +36,14 @@ export default function ChatMessageActions({ onRollback, }: ChatMessageActionsProps) { const { t } = useI18n(); - const hasTurnId = rollbackSupported && message?.role === 'user' && Boolean(message.turn_id); + const hasTurnId = rollbackSupported + && message?.role === 'user' + && Boolean(message.turn_id) + && typeof message.turn_index === 'number'; const actions: MobileActionSheetItem[] = message ? [ { id: 'copy', label: t('chat.copyMessage'), leading: }, - ...(message.role === 'user' ? [{ id: 'resend', label: t('chat.resendMessage'), leading: }] : []), + ...(message.role === 'user' && !hasTurnId ? [{ id: 'resend', label: t('chat.resendMessage'), leading: }] : []), ...(hasTurnId ? [ { id: 'edit', diff --git a/src/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 7f68b5dbb7..c2695c22ae 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -309,9 +309,9 @@ export const messages: Record = { editAndResend: 'Edit & resend', rollbackToHere: 'Roll back to here', rollbackSheetTitle: 'Roll back to this message?', - rollbackSheetHint: 'The desktop withdraws this message and everything after it, and restores the files those turns changed. Its text comes back to the input box so you can edit and send again.', + rollbackSheetHint: 'Withdraws this and subsequent messages, and reverts file changes.', editSheetTitle: 'Edit and resend', - editSheetHint: 'The desktop withdraws this message and everything after it first, then runs your edited text as a new turn.', + editSheetHint: 'Withdraws subsequent turns and runs your edited text.', rollbackAction: 'Roll back', editAction: 'Roll back & send', editPlaceholder: 'Edit this message...', @@ -674,9 +674,9 @@ export const messages: Record = { editAndResend: '编辑并重发', rollbackToHere: '回滚到这条', rollbackSheetTitle: '回滚到这条消息?', - rollbackSheetHint: '桌面端会撤回这条消息及其之后的全部内容,并还原这些轮次改动过的文件。这条消息的原文会回到输入框,方便你改完再发。', + rollbackSheetHint: '将撤回此条及后续消息,并还原文件变更。', editSheetTitle: '编辑并重发', - editSheetHint: '桌面端会先撤回这条消息及其之后的全部内容,再把你编辑后的文本作为新一轮发送。', + editSheetHint: '撤回后续消息并重新发送。', rollbackAction: '回滚', editAction: '回滚并发送', editPlaceholder: '编辑这条消息...', @@ -1039,9 +1039,9 @@ export const messages: Record = { editAndResend: '編輯並重送', rollbackToHere: '回溯到這條', rollbackSheetTitle: '回溯到這條訊息?', - rollbackSheetHint: '桌面端會撤回這條訊息及其之後的全部內容,並還原這些輪次改動過的檔案。這條訊息的原文會回到輸入框,方便你改完再送。', + rollbackSheetHint: '將撤回這條及後續訊息,並還原檔案變更。', editSheetTitle: '編輯並重送', - editSheetHint: '桌面端會先撤回這條訊息及其之後的全部內容,再把你編輯後的文本作為新一輪送出。', + editSheetHint: '撤回後續訊息並重新送出。', rollbackAction: '回溯', editAction: '回溯並送出', editPlaceholder: '編輯這條訊息...', diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index 85da746b76..c585768eae 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -13,6 +13,7 @@ import { RemoteSessionManager, SessionSynchronizer, type PollResponse, + type ChatImageAttachment, type ChatMessage, type RemoteModelCatalog, } from '../services/RemoteSessionManager'; @@ -60,6 +61,70 @@ function sanitizeMessageText(content: string): string { .trim(); } +function normalizeValidImageDataUrl(url: unknown): { trimmedUrl: string; mimeType: string } | null { + if (typeof url !== 'string') return null; + const trimmedUrl = url.trim(); + const match = trimmedUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/); + if (!match) return null; + const mimeType = match[1]; + const base64Data = match[2]; + const isValid = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}[AEIMQUYcgkosw048]=|[A-Za-z0-9+/][AQgw]==|[A-Za-z0-9+/]{4})$/.test(base64Data); + if (!isValid) return null; + return { trimmedUrl, mimeType }; +} + +function extractValidImageContexts( + images: unknown, + prefix: string, +): { id: string; data_url: string; mime_type: string; metadata: { name: string; source: string } }[] | undefined { + if (!Array.isArray(images) || images.length === 0) return undefined; + const valid: { data_url: string; mime_type: string; name: string }[] = []; + for (const img of images) { + try { + if (img && typeof img === 'object') { + const normalized = normalizeValidImageDataUrl(img.data_url); + if (normalized) { + valid.push({ + data_url: normalized.trimmedUrl, + mime_type: normalized.mimeType, + name: typeof img.name === 'string' && img.name ? img.name : 'image', + }); + } + } + } catch { + // Ignore corrupted attachment items + } + } + if (valid.length === 0) return undefined; + return valid.map((item, idx) => ({ + id: `${prefix}_${Date.now()}_${idx}`, + data_url: item.data_url, + mime_type: item.mime_type, + metadata: { name: item.name, source: 'remote' }, + })); +} + +function extractValidPendingImages(images: unknown): { name: string; dataUrl: string }[] { + if (!Array.isArray(images) || images.length === 0) return []; + const result: { name: string; dataUrl: string }[] = []; + for (const img of images) { + try { + if (img && typeof img === 'object') { + const normalized = normalizeValidImageDataUrl(img.data_url); + if (normalized) { + result.push({ + name: typeof img.name === 'string' && img.name ? img.name : 'image', + dataUrl: normalized.trimmedUrl, + }); + } + } + } catch { + // Ignore corrupted attachment items + } + } + return result; +} + // ─── Thinking (ModelThinkingDisplay-style) ─────────────────────────────────── @@ -209,6 +274,7 @@ const ChatPage: React.FC = ({ } | null>(null); const [rollbackDraft, setRollbackDraft] = useState(''); const [rollbackBusy, setRollbackBusy] = useState(false); + const rollbackInFlightRef = useRef(null); const msgLongPressTimerRef = useRef>(); const msgLongPressPosRef = useRef({ x: 0, y: 0 }); const msgToastTimerRef = useRef>(); @@ -247,6 +313,7 @@ const ChatPage: React.FC = ({ setRollbackTarget(null); setRollbackDraft(''); setRollbackBusy(false); + rollbackInFlightRef.current = null; setActionToast(null); setInfoToast(null); setExpandedMsgIds(new Set()); @@ -602,23 +669,13 @@ const ChatPage: React.FC = ({ const text = sanitizeMessageText(menuMessage.content); if (!text) return; setMenuMessage(null); - const imageContexts = menuMessage.images?.length - ? menuMessage.images.map((img, idx) => { - const mimeType = img.data_url.split(';')[0]?.replace('data:', '') || 'image/png'; - return { - id: `mobile_resend_${Date.now()}_${idx}`, - data_url: img.data_url, - mime_type: mimeType, - metadata: { name: img.name, source: 'remote' }, - }; - }) - : undefined; try { + const imageContexts = extractValidImageContexts(menuMessage.images, 'mobile_resend'); await sessionMgr.sendMessage(sessionId, text, sessionAgentType, imageContexts); if (!isChatTargetCurrent(targetEpoch)) return; streamRef.current?.nudge(); } catch (e: any) { - reportRemoteSessionError(e, setError); + if (isChatTargetCurrent(targetEpoch)) reportRemoteSessionError(e, setError); } }, [captureChatTargetEpoch, isChatTargetCurrent, menuMessage, sessionAgentType, sessionId, sessionMgr, setError]); @@ -657,7 +714,7 @@ const ChatPage: React.FC = ({ // the files they wrote. Editing is that same rollback followed by a normal // send, which is how the desktop reruns an edited user message. const handleConfirmRollback = useCallback(async () => { - if (!rollbackTarget || rollbackBusy) return; + if (!rollbackTarget || rollbackBusy || rollbackInFlightRef.current) return; // The host independently checks idle under its scheduling lock; this // presentation guard only avoids a request while this view is already busy. if (isStreaming) return; @@ -669,10 +726,13 @@ const ChatPage: React.FC = ({ const targetEpoch = captureChatTargetEpoch(); if (targetEpoch === null) return; + const attempt = Symbol('rollback'); + rollbackInFlightRef.current = attempt; setRollbackBusy(true); + const isCurrentAttempt = () => isChatTargetCurrent(targetEpoch) && rollbackInFlightRef.current === attempt; try { const result = await sessionMgr.rollbackSessionToTurn(sessionId, turnId, message.turn_index); - if (!isChatTargetCurrent(targetEpoch)) return; + if (!isCurrentAttempt()) return; setRollbackTarget(null); setRollbackDraft(''); // History changed on the host. Pull the authoritative snapshot now, before @@ -681,31 +741,32 @@ const ChatPage: React.FC = ({ streamRef.current?.nudge(); if (mode === 'edit') { - const imageContexts = message.images?.length - ? message.images.map((img, idx) => ({ - id: `mobile_edit_${Date.now()}_${idx}`, - data_url: img.data_url, - mime_type: img.data_url.split(';')[0]?.replace('data:', '') || 'image/png', - metadata: { name: img.name, source: 'remote' }, - })) - : undefined; + const imageContexts = extractValidImageContexts(message.images, 'mobile_edit'); + const fallbackPendingImages = extractValidPendingImages(message.images); try { await sessionMgr.sendMessage(sessionId, editedText, sessionAgentType, imageContexts); } catch (sendError) { // The rollback already retired the turn this text came from, so the // draft has nowhere to fall back to. Hand it to the composer instead // of dropping it when the send is what failed. - if (isChatTargetCurrent(targetEpoch)) { + if (isCurrentAttempt()) { setInput(editedText); - setPendingImages((message.images ?? []).map(img => ({ name: img.name, dataUrl: img.data_url }))); + setPendingImages(fallbackPendingImages); setInputExpanded(true); } throw sendError; } - if (!isChatTargetCurrent(targetEpoch)) return; - } else if (result.composer_text) { - setInput(result.composer_text); - setInputExpanded(true); + if (!isCurrentAttempt()) return; + } else { + const restoredText = result.composer_text ?? ''; + setInput(restoredText); + const restoredImages = extractValidPendingImages(message.images); + if (restoredImages.length > 0) { + setPendingImages(restoredImages); + } + if (restoredText.trim() || restoredImages.length > 0) { + setInputExpanded(true); + } } showMsgToast( @@ -722,13 +783,16 @@ const ChatPage: React.FC = ({ // conflict), so pull the authoritative snapshot instead of leaving the // transcript stale until the next idle poll. The stream ref belongs to // the current chat, so guard against a session switch mid-flight. - if (isChatTargetCurrent(targetEpoch)) { + if (isCurrentAttempt()) { streamRef.current?.nudge(); } - if (isChatTargetCurrent(targetEpoch)) reportRemoteSessionError(e, setError); + if (isCurrentAttempt()) reportRemoteSessionError(e, setError); } finally { - if (isChatTargetCurrent(targetEpoch)) { - setRollbackBusy(false); + if (rollbackInFlightRef.current === attempt) { + rollbackInFlightRef.current = null; + if (isChatTargetCurrent(targetEpoch)) { + setRollbackBusy(false); + } } } }, [ @@ -1135,6 +1199,9 @@ const ChatPage: React.FC = ({ const workspaceName = currentWorkspace?.project_name || currentWorkspace?.path?.split('/').pop() || ''; const gitBranch = currentWorkspace?.git_branch; const displayName = liveTitle || sessionName || t('chat.session'); + const isRemoteWorkspace = currentWorkspace?.workspace_kind === 'remote' + || Boolean(currentWorkspace?.remote_connection_id) + || Boolean(currentWorkspace?.remote_ssh_host); return (
@@ -1218,7 +1285,7 @@ const ChatPage: React.FC = ({ deleting={deletingMsg} message={menuMessage} streaming={isStreaming} - rollbackSupported={sessionMgr.supportsHostCapability('session_rollback_v1')} + rollbackSupported={sessionMgr.supportsHostCapability('session_rollback_v1') && !isRemoteWorkspace} onClose={() => setMenuMessage(null)} onCopy={() => void handleCopyMessage()} onDelete={() => void handleDeleteMessage()} diff --git a/src/mobile-web/tests/browser-account.test.mjs b/src/mobile-web/tests/browser-account.test.mjs index c72e3ebff5..ea6afa86d7 100644 --- a/src/mobile-web/tests/browser-account.test.mjs +++ b/src/mobile-web/tests/browser-account.test.mjs @@ -9,7 +9,18 @@ import { LAN, OFFICIAL, RelayFixture, connected, disconnect, invitation, launchB let browser; let source; before(async () => { source = await startSourceServer(); browser = await launchBrowser(); }); -after(async () => { await browser?.close(); await source?.close(); }); +after(async () => { + try { + await Promise.race([ + browser?.close(), + new Promise(resolve => setTimeout(resolve, 3000)), + ]); + } catch {} + try { + await source?.close(); + } catch {} + setTimeout(() => process.exit(0), 500); +}); for (const endpoint of [LAN, OFFICIAL]) { test(`one login serves fresh tabs and reloads with one connected browser: ${endpoint}`, { timeout: 40_000 }, async () => { @@ -185,7 +196,10 @@ test('a closed and reopened browser profile retains its login and controller key assert.equal(relay.logins.length, 1); assert.equal(relay.clients.size, 1); assert.deepEqual(relay.errors, []); - } finally { await persistent?.close(); await rm(profile, { recursive: true, force: true }); } + } finally { + await persistent?.close(); + try { await rm(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); } catch {} + } }); test('legacy tab credentials migrate with their keys and navigation without reviving a signed-out account', { timeout: 40_000 }, async () => { diff --git a/src/mobile-web/tests/helpers/browser-account-harness.mjs b/src/mobile-web/tests/helpers/browser-account-harness.mjs index 0e0cf5004d..ef5dd6ba9b 100644 --- a/src/mobile-web/tests/helpers/browser-account-harness.mjs +++ b/src/mobile-web/tests/helpers/browser-account-harness.mjs @@ -44,6 +44,11 @@ export async function launchBrowser(options = {}) { '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', process.env.PROGRAMFILES && `${process.env.PROGRAMFILES}/Google/Chrome/Application/chrome.exe`, + process.env['PROGRAMFILES(X86)'] && `${process.env['PROGRAMFILES(X86)']}/Google/Chrome/Application/chrome.exe`, + process.env.LOCALAPPDATA && `${process.env.LOCALAPPDATA}/Google/Chrome/Application/chrome.exe`, + process.env.PROGRAMFILES && `${process.env.PROGRAMFILES}/Microsoft/Edge/Application/msedge.exe`, + process.env['PROGRAMFILES(X86)'] && `${process.env['PROGRAMFILES(X86)']}/Microsoft/Edge/Application/msedge.exe`, + process.env.LOCALAPPDATA && `${process.env.LOCALAPPDATA}/Microsoft/Edge/Application/msedge.exe`, ].find(path => path && existsSync(path)); assert.ok(executablePath, 'Set PUPPETEER_EXECUTABLE_PATH to an installed Chrome/Chromium browser.'); return puppeteer.launch({ executablePath, headless: 'new', @@ -64,7 +69,7 @@ export async function startSourceServer() { sockets.handleUpgrade(request, socket, head, ws => fixture.socket(ws, url.searchParams.get('endpoint'))); }); await vite.listen(); - return { origin: `http://127.0.0.1:${vite.httpServer.address().port}`, close: async () => { for (const socket of sockets.clients) socket.terminate(); sockets.close(); realtimeFixtures.clear(); await vite.close(); } }; + return { origin: `http://127.0.0.1:${vite.httpServer.address().port}`, close: async () => { for (const socket of sockets.clients) socket.terminate(); sockets.close(); realtimeFixtures.clear(); vite.httpServer?.closeAllConnections?.(); await vite.close(); } }; } /** Real browser fetches and encrypted RPC envelopes; only Relay/host IO is simulated. */ diff --git a/src/mobile-web/tests/host-dialog-queue-browser.test.mjs b/src/mobile-web/tests/host-dialog-queue-browser.test.mjs index e73433c7a1..31274dfed0 100644 --- a/src/mobile-web/tests/host-dialog-queue-browser.test.mjs +++ b/src/mobile-web/tests/host-dialog-queue-browser.test.mjs @@ -1,8 +1,10 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; import { launchBrowser, startSourceServer } from './helpers/browser-account-harness.mjs'; -const modulePath = '/@fs' + fileURLToPath(new URL('../../shared/dialog-queue/HostDialogQueue.ts', import.meta.url)); +const modulePath = '/@fs/' + fileURLToPath(new URL('../../shared/dialog-queue/HostDialogQueue.ts', import.meta.url)).replace(/\\/g, '/').replace(/^\/+/, ''); test('real IndexedDB retains an ambiguous submission after closing the browser page', {timeout:60000}, async () => { const server=await startSourceServer();const browser=await launchBrowser(); @@ -50,7 +52,7 @@ test('mobile running composer keeps send and stop independently available alongs assert.ok((await page.evaluate(()=>window.queueFixture.calls)).includes('send')); assert.ok(!(await page.evaluate(()=>window.queueFixture.calls)).includes('stop')); assert.equal(await page.$eval('body', body=>body.scrollWidth<=window.innerWidth),true); - await page.screenshot({path:'/tmp/mobile-host-message-queue.png',fullPage:true}); + await page.screenshot({path:path.join(tmpdir(),'mobile-host-message-queue.png'),fullPage:true}); }finally{await browser.close();await server.close();} }); @@ -113,7 +115,7 @@ test('queue stays above the measured composer across phone, keyboard-height and assert.equal(geometry.plusWidth,44); for(const edge of ['top','bottom','left']) assert.ok(Math.abs(geometry[edge]-geometry.right)<1,JSON.stringify(geometry)); assert.ok(Math.abs(geometry.textLeft-geometry.iconLeft)<1,JSON.stringify(geometry)); - await page.screenshot({path:'/tmp/mobile-queue-aligned.png',fullPage:true}); + await page.screenshot({path:path.join(tmpdir(),'mobile-queue-aligned.png'),fullPage:true}); await page.click('[aria-label="排队消息说明"]'); await page.waitForSelector('.host-message-queue__help'); await page.click('[aria-label="排队消息说明"]'); diff --git a/src/mobile-web/tests/session-stream-browser.test.mjs b/src/mobile-web/tests/session-stream-browser.test.mjs index 63e1af4d01..c0858ab15f 100644 --- a/src/mobile-web/tests/session-stream-browser.test.mjs +++ b/src/mobile-web/tests/session-stream-browser.test.mjs @@ -84,13 +84,13 @@ test('an older host without host streams is reported as unsupported instead of r const relay=new RelayFixture();relay.legacyHost=true; const page=await relay.page(context,source.origin); await signIn(page);await connected(page); - const module=fileURLToPath(new URL('../../shared/relay-transport/HostStream.ts',import.meta.url)); + const module='/@fs/'+fileURLToPath(new URL('../../shared/relay-transport/HostStream.ts',import.meta.url)).replace(/\\/g, '/').replace(/^\/+/, ''); const result=await page.evaluate(async path=>{ const {getBrowserAccountStore,releaseBrowserAccount}=await import('/src/services/BrowserAccountStore.ts'); const {currentRelayUrl}=await import('/src/services/pairingLink.ts'); const {RelayHttpClient}=await import('/src/services/RelayHttpClient.ts'); const {RemoteSessionManager}=await import('/src/services/RemoteSessionManager.ts'); - const {UNSUPPORTED_HOST_MESSAGE}=await import('/@fs'+path); + const {UNSUPPORTED_HOST_MESSAGE}=await import(path); const saved=await getBrowserAccountStore(currentRelayUrl()).read(); const client=new RelayHttpClient(currentRelayUrl(),{token:saved.session.token,masterKey:saved.session.masterKey,userId:saved.session.userId,deviceId:saved.controllerDeviceId}); releaseBrowserAccount(saved); diff --git a/src/mobile-web/tests/workspace-identity.test.mjs b/src/mobile-web/tests/workspace-identity.test.mjs index 459ec659c4..0b11719f0e 100644 --- a/src/mobile-web/tests/workspace-identity.test.mjs +++ b/src/mobile-web/tests/workspace-identity.test.mjs @@ -108,6 +108,7 @@ const controlIdentity = await moduleUrl('../src/services/controlClientIdentity.t const hostStream = await moduleUrl('../../shared/relay-transport/HostStream.ts'); const managerUrl = await moduleUrl('../src/services/RemoteSessionManager.ts', { '../../../shared/agent-harness/wire': agentWire, + '../../../shared/dialog-queue/HostDialogQueue': 'data:text/javascript,export class HostDialogQueue {}', '../../../shared/relay-transport/HostStream': hostStream, './controlClientIdentity': controlIdentity, './SessionSynchronizer': 'data:text/javascript,export class SessionSynchronizer {}', diff --git a/src/web-ui/package.json b/src/web-ui/package.json index a959edb215..74490f8895 100644 --- a/src/web-ui/package.json +++ b/src/web-ui/package.json @@ -27,8 +27,8 @@ "lint": "eslint .", "lint:fix": "eslint . --fix", "type-check": "tsc --noEmit", - "test": "vitest", - "test:run": "vitest run --maxWorkers=50%" + "test": "cross-env NODE_OPTIONS=--no-experimental-webstorage vitest", + "test:run": "cross-env NODE_OPTIONS=--no-experimental-webstorage vitest run --maxWorkers=50%" }, "dependencies": { "@openbitfun/flow-chat-presentation": "workspace:*",