diff --git a/Makefile b/Makefile index a4af236f865..ecc1dff2ec9 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,7 @@ include fmt.mk .PHONY: all build dev start clean help .PHONY: build-renderer version build-icons build-static build-docker-runtime verify-docker-runtime-artifacts .PHONY: lint lint-fix typecheck static-check static-check-full -.PHONY: test test-unit test-unit-ci test-integration test-watch test-coverage test-e2e test-e2e-perf smoke-test +.PHONY: test test-unit test-unit-ci test-integration test-watch test-coverage test-e2e test-e2e-perf perf-tape-replay smoke-test .PHONY: dist dist-mac dist-win dist-linux install-mac-arm64 ensure-mac-sharp-runtime-deps check-appimage-icons check-mac-attach-file-runtime .PHONY: vscode-ext vscode-ext-install .PHONY: docs-server check-docs-links @@ -549,6 +549,10 @@ test-e2e-perf: ## Run automated performance profiling scenarios @# One worker: parallel Electron apps contend on CPU, so a scenario measures its neighbours' startup (#5209). @XUM_E2E_RUN_PERF=1 XUM_PROFILE_REACT=1 XUM_E2E_LOAD_DIST=1 XUM_E2E_SKIP_BUILD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun x playwright test --project=electron tests/e2e/scenarios/perf*.spec.ts --workers 1 $(PLAYWRIGHT_ARGS) +perf-tape-replay: ## Replay a synthetic session tape through the desktop app (perf.tapeReplay) + @$(MAKE) build + @XUM_E2E_RUN_PERF=1 XUM_PROFILE_REACT=1 XUM_E2E_LOAD_DIST=1 XUM_E2E_SKIP_BUILD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun x playwright test --project=electron tests/e2e/scenarios/perf.tapeReplay.spec.ts --workers 1 $(PLAYWRIGHT_ARGS) + ## Distribution dist: build ## Build distributable packages @bun x electron-builder --publish never diff --git a/scripts/perf/generateTapeReplayFixture.ts b/scripts/perf/generateTapeReplayFixture.ts new file mode 100644 index 00000000000..32e1a4de884 --- /dev/null +++ b/scripts/perf/generateTapeReplayFixture.ts @@ -0,0 +1,23 @@ +/** + * Regenerate the synthetic session tape fixture of the `perf.tapeReplay` e2e scenario: + * bun scripts/perf/generateTapeReplayFixture.ts + */ +import { writeFileSync } from "node:fs"; +import * as path from "node:path"; +import { + buildTapeReplayFixtureTape, + TAPE_REPLAY_FIXTURE_FILE_NAME, +} from "../../tests/e2e/fixtures/sessionTapes/tapeReplayFixture"; + +const target = path.join( + import.meta.dir, + "..", + "..", + "tests", + "e2e", + "fixtures", + "sessionTapes", + TAPE_REPLAY_FIXTURE_FILE_NAME +); +writeFileSync(target, buildTapeReplayFixtureTape()); +console.log(`Wrote ${path.relative(process.cwd(), target)}`); diff --git a/scripts/perf/tapeReplayFixture.test.ts b/scripts/perf/tapeReplayFixture.test.ts new file mode 100644 index 00000000000..cb6bdf9abde --- /dev/null +++ b/scripts/perf/tapeReplayFixture.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test"; +import * as path from "node:path"; +import { hashSessionTapeWorkspaceId } from "@/node/services/sessionTapes/sessionTapeRecorder"; +import { readSessionTapeFile } from "@/node/services/sessionTapes/sessionTapeFile"; +import { + TAPE_REPLAY_FIXTURE_FILE_NAME, + TAPE_REPLAY_FIXTURE_WORKSPACE_ID, +} from "../../tests/e2e/fixtures/sessionTapes/tapeReplayFixture"; + +const FIXTURE_PATH = path.join( + import.meta.dir, + "..", + "..", + "tests", + "e2e", + "fixtures", + "sessionTapes", + TAPE_REPLAY_FIXTURE_FILE_NAME +); + +describe("perf.tapeReplay fixture tape", () => { + test("loads as a complete tape recorded for the scenario's workspace id", async () => { + const result = await readSessionTapeFile(FIXTURE_PATH); + expect(result.status).toBe("ok"); + if (result.status !== "ok") return; + // The replay source refuses a tape whose hash does not match the mapped workspace. + expect(result.header.workspaceIdHash).toBe( + hashSessionTapeWorkspaceId(TAPE_REPLAY_FIXTURE_WORKSPACE_ID) + ); + }); +}); diff --git a/src/browser/components/StartHereModal/StartHereModal.tsx b/src/browser/components/StartHereModal/StartHereModal.tsx index 69f565ed222..f4ebc41733a 100644 --- a/src/browser/components/StartHereModal/StartHereModal.tsx +++ b/src/browser/components/StartHereModal/StartHereModal.tsx @@ -10,6 +10,7 @@ import { import { Button } from "@/browser/components/Button/Button"; import { stopKeyboardPropagation } from "@/browser/utils/events"; import { isEditableElement, KEYBINDS, matchesKeybind } from "@/browser/utils/ui/keybinds"; +import { getErrorMessage } from "@/common/utils/errors"; interface StartHereModalProps { isOpen: boolean; @@ -30,9 +31,12 @@ export const StartHereModal: React.FC = ({ confirmDisabled = false, }) => { const [isExecuting, setIsExecuting] = useState(false); + // Why the last confirmation was refused; shown until the next attempt or cancel. + const [error, setError] = useState(undefined); const handleCancel = useCallback(() => { if (!isExecuting) { + setError(undefined); onClose(); } }, [isExecuting, onClose]); @@ -40,11 +44,13 @@ export const StartHereModal: React.FC = ({ const handleConfirm = useCallback(async () => { if (isExecuting || confirmDisabled) return; setIsExecuting(true); + setError(undefined); try { await onConfirm(); onClose(); - } catch (error) { - console.error("Start Here error:", error); + } catch (confirmError) { + setError(getErrorMessage(confirmError)); + } finally { setIsExecuting(false); } }, [isExecuting, confirmDisabled, onConfirm, onClose]); @@ -88,6 +94,11 @@ export const StartHereModal: React.FC = ({ This will start a new context from this message and preserve earlier chat history. + {error && ( +
+ {error} +
+ )}