Skip to content

[local-explorer-ui] Add email view and sending emails to workers to the local explorer - #15337

Open
tpmmorris wants to merge 3 commits into
cloudflare:mainfrom
tpmmorris:tmorris/email-explorer-ui
Open

[local-explorer-ui] Add email view and sending emails to workers to the local explorer#15337
tpmmorris wants to merge 3 commits into
cloudflare:mainfrom
tpmmorris:tmorris/email-explorer-ui

Conversation

@tpmmorris

@tpmmorris tpmmorris commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #13648

Adds email inspection and sending of test emails to workers to the local explorer.

The new email group has two tabs - routing and sending - which provide users with inboxes of emails sent from/to workers respectively. Within these views, users can open detail views to see message content, and for received emails the handling path.

Emails can be composed and delivered to workers, which provides access to standard fields, as well as custom headers, html, and attachments.

image
image
image

@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2e5762f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@cloudflare/local-explorer-ui Minor
miniflare Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-plugin Patch
wrangler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Aug 24, 2026
@workers-devprod
workers-devprod requested review from a team and penalosa and removed request for a team August 24, 2026 16:58
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/email-local-explorer-ui.md: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/e2e/email-sending.spec.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/e2e/worker-selector.spec.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/tests/utils/email-html.test.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/tests/utils/sidebar-state.test.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/tests/utils/worker-navigation.test.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/assets/icons/email.svg: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/Sidebar.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/EmailHtmlPreview.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/EmailList.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/EmailPagination.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/EmailServiceEmptyState.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/EmailTruncationWarning.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/ReceivedEmailContent.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/SendTestEmailDialog.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/components/email/SentEmailDetails.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routeTree.gen.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/__root.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/routing.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/routing/$emailId.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/routing/index.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/sending.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/ConstantsCard.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/EventNode.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/InfoFlow.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/InfoLoading.tsx: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/format.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/types.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/routes/email/shared/useCursorPaginatedList.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/utils/email-html.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/utils/sidebar-state.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/src/utils/worker-navigation.ts: [@cloudflare/wrangler]
  • packages/local-explorer-ui/tsconfig.e2e.json: [@cloudflare/wrangler]
  • packages/local-explorer-ui/vite.config.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/contracts.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/email/mime.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/types.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/openapi.local.json: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/resources/email.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/local-explorer/email.spec.ts: [@cloudflare/wrangler]

@pkg-pr-new

pkg-pr-new Bot commented Aug 24, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15337

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15337

@cloudflare/codemods

npm i https://pkg.pr.new/@cloudflare/codemods@15337

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15337

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15337

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15337

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15337

miniflare

npm i https://pkg.pr.new/miniflare@15337

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15337

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15337

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15337

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15337

@cloudflare/vitest-plugin

npm i https://pkg.pr.new/@cloudflare/vitest-plugin@15337

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15337

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15337

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15337

wrangler

npm i https://pkg.pr.new/wrangler@15337

commit: 2e5762f

@emily-shen
emily-shen requested review from emily-shen and a lite review from Copilot and removed request for penalosa August 25, 2026 09:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Local Explorer email inspection, routing, and test-email sending across the Miniflare API and UI.

Changes:

  • Extends email APIs, contracts, schemas, MIME handling, and validation.
  • Adds routing and sending views, composition, previews, pagination, and worker navigation.
  • Adds unit, API, and E2E tests plus release metadata.

Reviewed changes

Copilot reviewed 38 out of 41 changed files in this pull request and generated 2 comments.

Show a summary per file
File Summary
packages/miniflare/test/plugins/local-explorer/email.spec.ts Email API coverage
packages/miniflare/src/workers/local-explorer/resources/email.ts Email parsing and validation
packages/miniflare/src/workers/local-explorer/openapi.local.json API schema updates
packages/miniflare/src/workers/local-explorer/generated/zod.gen.ts Generated validation schemas
packages/miniflare/src/workers/local-explorer/generated/types.gen.ts Generated API types
packages/miniflare/src/workers/email/mime.ts MIME header handling
packages/miniflare/src/workers/email/contracts.ts Email API contracts
packages/local-explorer-ui/vite.config.ts Route generation configuration
packages/local-explorer-ui/tsconfig.e2e.json E2E type inclusion
packages/local-explorer-ui/src/utils/worker-navigation.ts Worker-switch navigation
packages/local-explorer-ui/src/utils/sidebar-state.ts Email sidebar state
packages/local-explorer-ui/src/utils/email-html.ts Safe HTML previews
packages/local-explorer-ui/src/routeTree.gen.ts Generated email routes
packages/local-explorer-ui/src/routes/email/shared/useCursorPaginatedList.ts Cursor pagination. Moderate (2 votes): stale request rejections can update the error after a worker switch.
packages/local-explorer-ui/src/routes/email/shared/types.ts Shared email types
packages/local-explorer-ui/src/routes/email/shared/InfoLoading.tsx Loading state
packages/local-explorer-ui/src/routes/email/shared/InfoFlow.tsx Email event flow
packages/local-explorer-ui/src/routes/email/shared/format.ts Email formatting
packages/local-explorer-ui/src/routes/email/shared/EventNode.tsx Event timeline nodes
packages/local-explorer-ui/src/routes/email/shared/ConstantsCard.tsx Event metadata display
packages/local-explorer-ui/src/routes/email/sending.tsx Sending route
packages/local-explorer-ui/src/routes/email/routing/index.tsx Routing list route
packages/local-explorer-ui/src/routes/email/routing/$emailId.tsx Routing detail route
packages/local-explorer-ui/src/routes/email/routing.tsx Routing layout
packages/local-explorer-ui/src/routes/__root.tsx Root navigation integration
packages/local-explorer-ui/src/components/Sidebar.tsx Email sidebar entry
packages/local-explorer-ui/src/components/email/SentEmailDetails.tsx Sent-email details
packages/local-explorer-ui/src/components/email/SendTestEmailDialog.tsx Test-email composer
packages/local-explorer-ui/src/components/email/ReceivedEmailContent.tsx Received-email content
packages/local-explorer-ui/src/components/email/EmailTruncationWarning.tsx Truncation warning
packages/local-explorer-ui/src/components/email/EmailServiceEmptyState.tsx Empty state
packages/local-explorer-ui/src/components/email/EmailPagination.tsx Pagination controls
packages/local-explorer-ui/src/components/email/EmailList.tsx Email list rendering
packages/local-explorer-ui/src/components/email/EmailHtmlPreview.tsx HTML preview
packages/local-explorer-ui/src/assets/icons/email.svg Email icon
packages/local-explorer-ui/src/__tests__/utils/worker-navigation.test.ts Navigation tests
packages/local-explorer-ui/src/__tests__/utils/sidebar-state.test.ts Sidebar tests
packages/local-explorer-ui/src/__tests__/utils/email-html.test.ts HTML preview tests
packages/local-explorer-ui/src/__e2e__/worker-selector.spec.ts Worker-selection E2E tests. Critical (3 votes): locators at lines 384, 443, and 447 use a link role although email rows render as buttons, so the test fails before exercising worker switching.
packages/local-explorer-ui/src/__e2e__/email-sending.spec.ts Email-sending E2E tests
.changeset/email-local-explorer-ui.md Release metadata
Suppressed comments (8)

packages/local-explorer-ui/src/e2e/worker-selector.spec.ts:443

  • The email rows are rendered as buttons, so this link-role locator cannot find Worker two email and the test will fail after the worker switch. Use the button role to match EmailList.
		await page.getByRole("link", { name: /Worker two email/ }).waitFor();

packages/local-explorer-ui/src/e2e/worker-selector.spec.ts:447

  • This is the same row-role mismatch as above: EmailList uses buttons, so the assertion queries no email rows and will not verify that the old row disappeared. Query the button role instead.
		expect(await page.getByRole("link", { name: /Old email/ }).count()).toBe(0);

packages/local-explorer-ui/src/components/email/EmailList.tsx:65

  • The refresh control remains enabled while disabled is true for pagination. Clicking Refresh during an in-flight next/previous request starts a second loadPage; the responses can then leave the displayed items and cursor history out of sync. Disable this button with the same disabled state (or otherwise serialize the operations).
						disabled={refreshing}

packages/local-explorer-ui/src/components/email/SendTestEmailDialog.tsx:73

  • The parser populates this ordinary object through dynamic keys. A valid HTTP field name such as __proto__ invokes Object.prototype's setter on {} rather than creating an enumerable header, so that custom header is silently omitted from the request. Use a null-prototype record or collect entries with Object.fromEntries.
	const headers: Record<string, string> = {};

packages/local-explorer-ui/src/components/email/SendTestEmailDialog.tsx:196

  • This dialog-wide error is cleared whenever an attachment finishes reading. That hides unrelated validation errors (for example, a missing sender) and can also erase the capacity error from a concurrent rejected selection, even though the form is still invalid or the file was not added. Clear attachment errors when starting an accepted selection, or keep attachment and form errors separate.
			setError(null);

packages/local-explorer-ui/src/components/email/SendTestEmailDialog.tsx:175

  • The limit check allows a total exactly equal to MAX_TOTAL_ATTACHMENT_BYTES (> rejects only values above it), but this message says the total must be “less than” the limit. Use wording such as “at most” so the boundary behavior is accurately communicated.
				`Attachments must total less than ${formatSize(MAX_TOTAL_ATTACHMENT_BYTES)}.`

packages/local-explorer-ui/src/components/email/SentEmailDetails.tsx:169

  • A capture can set truncated while omitting both text and html when even their empty representations do not fit in the row; raw can be absent in the same situation. This branch ignores truncated and instead reports that the email has no captured body, hiding the important truncation warning. Render EmailTruncationWarning when truncated is true here as well.
				{!email.text && !email.html && !email.raw ? (
					<div className="rounded-lg border border-kumo-fill bg-kumo-elevated px-5 py-8 text-center text-sm text-kumo-subtle">
						This email has no captured text or HTML body.
					</div>
				) : null}

packages/local-explorer-ui/src/routes/email/routing/$emailId.tsx:46

  • The API emits a separate warning when a captured reply is truncated, but this loader only checks for the primary received-email warning. As a result, EventNode can render a partial reply.raw without any warning. Propagate the reply-truncation state to the affected reply event and show the warning there without marking the complete primary message as truncated.
		const truncated = hasEmailTruncationWarning(
			response.data?.messages ?? [],
			"received"
		);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

viteUrl
).toString()
);
await page.getByRole("link", { name: /Old email/ }).waitFor();
Comment on lines +70 to +76
const page = await fetchPage(cursor);
if (requestId !== request.current) {
return false;
}
setItems(page.items);
setNextCursor(page.nextCursor);
return true;
@tpmmorris
tpmmorris force-pushed the tmorris/email-explorer-ui branch from b4664ef to b3a90b7 Compare August 25, 2026 09:45
@tpmmorris
tpmmorris force-pushed the tmorris/email-explorer-ui branch from b3a90b7 to 2e5762f Compare August 25, 2026 10:18
@emily-shen

Copy link
Copy Markdown
Contributor

Misc feedback, not necessarily all for this PR

Email routing:

  • form errors should be highlighted on the field itself, rather than a banner at the top - e.g. like this https://kumo-ui.com/components/input/#with-error-string
  • form errors should show up before you submit
    (sorry the form stuff is a bit annoying but being able to bully an LLM into producing a good form
  • message id should not include angle braces
  • message summary should include all information including headers, bcc/cc.
  • maybe the 'sent' message should be included in the 'received' lifecycle event?
  • Add a 'resend' button so you don't have to fill out the form constantly
  • maybe add a note to the 'No emails received yet' placeholder that this won't work if no email handler is configured.
  • sent emails with no email handler should look more 'incorrect' in the routing email list. maybe a red <!> icon? tbh i still think these should not be captured and should fail entirely. i wonder if we can have a 'probe' API endpoint that tries to send some dummy email and we fail earlier in the UI if that doesn't go through - that would defs be a followup, just throwing ideas out there.

email sending:

  • download attachments (are they stored?)
  • for the message summary, what does and doesn't have angle braces is a bit inconsistent (e.g. from and id but not to and cc
  • hide 'sending' from sidebar if binding is not configured?
  • 'worker' field in message summary is redundant as the whole UI is filtered per worker anyway.

both:

  • raw option for html
  • Consider where a strategically placed tooltip might be helpful

also maybe worth throwing https://kumo-ui.com/skill.md into an llm for a review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

4 participants