Skip to content

fix(webapp): show the toast when saving project general settings - #4601

Merged
carderne merged 5 commits into
mainfrom
fix/project-settings-toast-lost-on-redirect
Aug 13, 2026
Merged

fix(webapp): show the toast when saving project general settings#4601
carderne merged 5 commits into
mainfrom
fix/project-settings-toast-lost-on-redirect

Conversation

@claude

@claude claude Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Requested by Chris Arderne · Slack thread

Related to #4324

Before / After

Before: an org member without project-manage permission renames a project on the general settings page, hits Save, and lands on the tasks page with no message at all. The code intends to show "You don't have permission to rename this project", but nothing appears — the rename silently does nothing. The same silence applied to a successful rename: the "Project renamed to ..." toast never showed either.

After: you stay on the general settings page you submitted from, and the toast explains what happened — either the permission error or the rename confirmation.

This makes the reason visible; it does not change who is allowed to rename a project.

Why the message disappeared

The toast is a flash value in the __message cookie. The root loader (app/root.tsx) reads it with session.get("toastMessage") — which deletes the flash — and commits the emptied session on the same response. So the message survives exactly one hop that runs the root loader.

These redirects pointed at the project root, /orgs/:org/projects/:project. That route's _index loader doesn't render anything; it immediately redirects again to the environment's tasks page. On a client navigation Remix fetches every matched route's loader in parallel, so the root loader ran for that intermediate hop, spent the flash, and returned a Set-Cookie clearing it — and then the router threw that hop's data away because a sibling loader redirected. By the time the tasks page rendered, the cookie was already empty.

Ruled out along the way: the Set-Cookie is not dropped on the action's redirect, shouldRevalidate is not blocking the reader, and nothing swallows the thrown Response.

Worth knowing generally: any redirectWithSuccessMessage / redirectWithErrorMessage whose target is itself a redirecting route loses its message for this reason, since the flash is read and cleared on a hop that renders nothing. Other call sites are likely affected. Fixing the class properly — forwarding the Set-Cookie through internal redirects, or reading the flash somewhere other than root — is a bigger architectural change than this PR should carry, so it deliberately stays out of scope: this PR only fixes the call sites in this one route.

How

redirectWithErrorMessage / redirectWithSuccessMessage in the general settings action now target v3ProjectSettingsGeneralPath(...) instead of v3ProjectPath(...). That removes the intermediate redirect hop entirely, so the very next request renders the toast.

Redirecting back to the form is also the better behaviour on its own terms — a save that fails shouldn't move you off the page you were working on. The alternative fixes were worse: reading the flash somewhere other than root would need the toast plumbing rewritten for one call site, and having the project _index loader forward the cookie would leave the same trap set for every other caller that redirects through it. redirectDocument() was considered (per #4146's sibling fix for stale route chunks) and isn't relevant here — the settings route is already loaded, and this was never a chunk-loading failure.

Applied to all four project-root redirects in the file: rename denial, rename success, delete denial, and delete failure.

Delete success has the same defect and is deliberately left alone. It redirects to the org root /orgs/:org, whose _index loader has no branch that renders — every path throws a redirect — so "Project deleted" is swallowed the same way. The project you'd return to no longer exists, and there is no org-level page that both renders and makes sense to land on after a delete (the org has no projects list; the only rendering org pages are the settings subtree, "new project" and invites). Relocating where people land after deleting something is a worse trade than a missing confirmation, so the destination stays as it is and the release note doesn't promise a message for deletion.

The default-region action in …env.$envParam.regions/route.tsx was checked — it already redirects to regionsPath(...), its own rendering page, so it never lost its message and is unchanged.

The other silent path

With the redirects fixed, the two json({ errors: { body: … } }) returns were the only remaining way to fail this form with nothing on screen. That shape isn't a conform SubmissionResult, so when it reached useForm({ lastResult }) conform called reset() and threw the message away: blank form, no error, submit re-enabled. Both now return json(submission.reply({ formErrors: [...] })), and the rename form renders renameForm.errors the way the delete form on the same page already did.

The as any on useActionData() stays. dashboardAction is generic over TReturn extends Response, so useActionData<typeof action>() resolves to unknown and the cast is the only thing keeping it compiling. Removing it properly means teaching the shared route builder to carry its handler's payload type, which would touch every route using it.


✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

New test at apps/webapp/test/projectSettingsToastRedirect.test.ts, four cases:

  1. Drives the real redirectWithErrorMessage and the real cookie session storage through the two-hop chain and asserts the message is present on the first hop and gone on the second — the reproduction of the bug.
  2. and 3. Drive the route's real action for a denied rename and a denied delete, asserting both the redirect Location and that a root-loader-style read of the resulting cookie yields the expected message.
  3. Pins the successful-delete destination at the org root, so the deliberate trade above doesn't drift unnoticed.
  4. Drives a failing rename and asserts the response is a conform SubmissionResult carrying the form-level error, so the message actually reaches the form.

Cases 2 and 3 fail against main (expected '/orgs/o/projects/p' to be '/orgs/o/projects/p/env/prod/settings/general') and pass with this change.

To be clear about what that does and doesn't cover: these tests drive the real redirectWithErrorMessage, the real cookie session storage and the route's real action (the auth gate and ProjectSettingsService are mocked, as in the existing route tests). I have not clicked through a running dashboard — the redirect target and the cookie lifecycle are what's verified here.

Test Files  1 passed (1)
     Tests  5 passed (5)

Also run: pnpm run typecheck --filter webapp (19/19 tasks pass), pnpm run lint --filter webapp (clean — 7 pre-existing warnings, unchanged from main) and pnpm exec oxfmt --check . (clean).


Changelog

Renaming a project now keeps you on the project settings page and tells you what happened, instead of silently moving you to the tasks page or clearing the form with no explanation.


Screenshots

None — the change is a redirect target; the toast component itself is unchanged.

💯

The permission-denied and success toasts are flashed into the `__message`
cookie, but the root loader consumes the flash on every hop and these
redirects targeted the project root, whose index loader immediately
redirects again. The message was read and cleared on a page that never
rendered, so nothing was ever shown.

Redirect back to the settings page the form was submitted from instead.

Co-Authored-By: Claude <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 617296a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@claude claude Bot added the preview label Aug 13, 2026
@carderne
carderne marked this pull request as ready for review August 13, 2026 10:03
@trigger-dot-bot

trigger-dot-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

Preview Deployment

Status Preview Commit Updated
⚪ Removed 617296a Aug 13, 11:23 UTC

devin-ai-integration[bot]

This comment was marked as resolved.

The delete success path redirected to the org root, whose index loader
has no rendering branch — every path throws a redirect — so "Project
deleted" was spent on a non-rendering hop like the others. Send it to
the organization settings page, the nearest ancestor that renders.

Co-Authored-By: Claude <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

No org-level page both renders and makes sense to land on after deleting
a project, so moving the destination to reach the toast was a worse
trade than the missing confirmation. Restore the org root and reword the
release note so it only promises what renaming actually delivers.

Co-Authored-By: Claude <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

The two json({ errors: { body } }) returns aren't a conform
SubmissionResult, so conform reset the form and discarded them — the
last way to fail this form with nothing on screen. Return
submission.reply({ formErrors }) and render the rename form's
form-level errors, matching the delete form on the same page.

Co-Authored-By: Claude <noreply@anthropic.com>

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread apps/webapp/test/projectSettingsToastRedirect.test.ts
Both conform forms on the project general settings page read the same
action result, and a SubmissionResult carries no form identity, so a
failed rename rendered its form-level error under the delete box too.

Echo the submitted intent back as `formAction` and gate each form's
lastResult on it, matching the test task page.

Co-Authored-By: Claude <noreply@anthropic.com>
@carderne
carderne merged commit 802d238 into main Aug 13, 2026
42 checks passed
@carderne
carderne deleted the fix/project-settings-toast-lost-on-redirect branch August 13, 2026 11:17
@github-actions github-actions Bot mentioned this pull request Aug 13, 2026
ericallam pushed a commit that referenced this pull request Aug 13, 2026
## Summary
4 new features, 24 improvements, 10 bug fixes.

## Highlights

- Allow `trigger deploy` to authenticate with an environment API key
from `TRIGGER_ACCESS_TOKEN`.
([#4561](#4561))

## Improvements
- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- The dev environment onboarding now tracks real progress. After you run
`init`, the setup checklist marks your project as initialized, and it
updates live as your dev server connects and your tasks register. The
blank state also adds a "Copy AI agent prompt" button that copies a
ready-to-paste setup prompt (pre-filled with your project reference) for
Claude Code, Cursor, or any coding agent.
([#4563](#4563))
  
The `init` scaffold now imports from `@trigger.dev/sdk` instead of the
deprecated `@trigger.dev/sdk/v3` subpath.
- Deployed images now ship dependencies and bundled task code as
separate layers. Repeat deploys with unchanged dependencies typically
push and pull far less data, making deploys and worker image pulls
faster.
([#4551](#4551))
- The current-worker API now reports each task's queue, so you can see
which tasks write to a given queue.
([#4525](#4525))
- Watch-mode chat streams now survive quiet windows and page reloads,
and a reply cut off by a lost connection shows an error instead of
appearing finished. Aborting a resumed subscription only closes your
local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true`
to stop the run. Also fixed a race where quickly restarting a stream
could break stop and reconnect, and stopping a chat now hands it back to
your other tabs instead of leaving them read-only.
([#4516](#4516))

## Server changes

These changes affect the self-hosted Docker image and Trigger.dev Cloud:

- The dashboard agent now has a monthly message allowance and plan-based
limits on watches. Queries stay read-only with clearer errors when busy,
and messages with unusual characters no longer fail to send.
([#4516](#4516))
- Meet the dashboard agent: a chat in every environment that answers
questions about your runs, queues, errors and health with real data and
links, replacing Ask AI everywhere it used to appear. Investigate a
failed run, an error, a backed-up queue or a run that hasn't started to
get a worked-through answer — what happened, why, and how to fix it,
with every claim linked to the runs, errors and deploys behind it. It
reads your data read-only, works on preview and dev branches with that
branch's own data, and reads the same everywhere — dashboard, terminal,
editor. A very long chat keeps working: the agent summarises the earlier
part and carries on.
  
**Watch…** on a run, queue, error or the health report tells you when
things change: a run finishes, a queue clears or grows past a number you
pick, an error comes back, an environment recovers. The answer arrives
in the chat and, if you want, by email, Slack or webhook — and the agent
can look into bad news on its own. A watch reaches you on any browser
you sign in from, without opening the chat first.
  
A sample of conversations is scored automatically so the agent keeps
getting better; only the score and a one-line summary are kept, never
your messages, data or code, and we can switch it off for your
organization on request. Ask the agent instead of the Docs buttons in
page headers — they stay there when the agent isn't available to you.
Separately, a queue's wait times, peak depth, throughput and throttling
can now be read from the API.
([#4418](#4418))
- Add backend support for delaying cron schedules within a specified
window with a minimum of 60 seconds.
([#4566](#4566))
- Reduced recurring background database load from the billing-limit
recovery check, so paused environments are reconciled with less
overhead.
([#4590](#4590))
- Validating a schedule when deploying or updating a schedule now does
less work on projects with many preview branches, so those operations
stay fast as branches accumulate.
([#4598](#4598))
- Project pages now load faster for projects with a large number of
preview branches, by no longer loading archived branch environments that
aren't shown.
([#4595](#4595))
- Database queries that filter on a list of values now reuse cached
query plans more consistently, instead of forcing the database to
re-plan whenever the list length changes.
([#4480](#4480))
- Routine cleanup of old dashboard agent data now runs on its own
schedule.
([#4599](#4599))
- Database connection metrics are now reported for every configured
database connection instead of only the primary one, and stay accurate
regardless of connection type.
([#4541](#4541))
- Deployment-related API endpoints now draw from their own generous rate
limit budget, configurable via the `DEPLOYMENT_RATE_LIMIT_*` environment
variables, so runtime API traffic no longer competes with deployments
for the same per-environment budget.
([#4565](#4565))
- Deleting or editing a secret environment variable is now fast and no
longer slows down as a project accumulates variables.
([#4555](#4555))
- Speed up personal access token lookups by indexing them on their owner
([#4588](#4588))
- Switching project or organization in the sidebar now keeps you on the
same page instead of sending you back to Tasks. Pages for a specific
run, deploy or other single item open the matching list instead.
([#4585](#4585))
- Reduced database load when loading the dashboard by removing an unused
organization member count that was being calculated on every page
navigation.
([#4587](#4587))
- The environment variables page now loads a page at a time, keeping it
fast for projects with a large number of variables. Search matches
variable names across every page.
([#4597](#4597))
- Groundwork for an alternative database connection driver, gated behind
configuration and disabled by default, so there is no change to default
behavior.
([#4539](#4539))
- Deleting an alert channel is now fast and no longer slows down as a
project builds up alert history.
([#4554](#4554))
- Reduced internal overhead on the API under high load.
([#4532](#4532))
- Out-of-date upgrade prompts no longer appear in the dashboard: the
"V4" badges and the notices saying preview branches and the queues table
need V4 have been removed. The side menu still warns you when a project
is on v3, with updated wording and a link to the v4 upgrade guide.
([#4589](#4589))
- Make background worker registration cheaper for projects with many
scheduled tasks by scoping declarative schedule reconciliation to the
current environment and dropping redundant schedule lookups.
([#4577](#4577))
- Speed up setting and importing environment variables for projects with
many variables.
([#4579](#4579))
- Loading the deployments list is now faster, especially when filtering
by deployment status on projects with many deployments.
([#4591](#4591))
- Fixed the billing limits page timing out for organizations with many
preview branches, especially while a spend limit was being enforced. The
page now loads quickly, so you can raise or resolve your limit without
delay. ([#4594](#4594))
- Fix the Concurrency page showing the plan's default concurrency for
the dev environment instead of the environment's actual limit.
([#4596](#4596))
- Creating an organization sometimes left you back on the creation form
even though the organization had already been created, so clicking
Create again made a duplicate. Creating an organization now completes
and takes you to your new organization.
([#4530](#4530))
- Ensure creating a project completes instead of returning to its
creation form after a navigation error.
([#4584](#4584))
- Renaming a project now keeps you on the project settings page and
tells you what happened, instead of silently moving you to the tasks
page or clearing the form with no explanation.
([#4601](#4601))
- Fixed support threads showing no account details for some customers,
so the team can see your plan, organizations and projects when you get
in touch.
([#4575](#4575))
- In the light theme, the Format, Clear and Copy buttons on the query
editor no longer blend into the query text behind them.
([#4592](#4592))
- The health report now says start latency is "unknown" when there is no
data for it, instead of showing a healthy-looking 0ms
([#4544](#4544))
- Realtime streams written inside a chat session run now use the same
backend as the session itself, and runs are no longer created against a
backend that cannot serve them.
([#4564](#4564))
- The grouped "watch updates" notification now shows the total number of
results waiting, instead of only the most recent batch's count.
([#4525](#4525))

<details>
<summary>Raw changeset output</summary>

# Releases
## @trigger.dev/build@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## trigger.dev@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- Allow `trigger deploy` to authenticate with an environment API key
from `TRIGGER_ACCESS_TOKEN`.
([#4561](#4561))
- The dev environment onboarding now tracks real progress. After you run
`init`, the setup checklist marks your project as initialized, and it
updates live as your dev server connects and your tasks register. The
blank state also adds a "Copy AI agent prompt" button that copies a
ready-to-paste setup prompt (pre-filled with your project reference) for
Claude Code, Cursor, or any coding agent.
([#4563](#4563))

The `init` scaffold now imports from `@trigger.dev/sdk` instead of the
deprecated `@trigger.dev/sdk/v3` subpath.

- Deployed images now ship dependencies and bundled task code as
separate layers. Repeat deploys with unchanged dependencies typically
push and pull far less data, making deploys and worker image pulls
faster.
([#4551](#4551))
- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
  - `@trigger.dev/build@4.5.11`
  - `@trigger.dev/schema-to-json@4.5.11`
## @trigger.dev/core@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- The current-worker API now reports each task's queue, so you can see
which tasks write to a given queue.
([#4525](#4525))
## @trigger.dev/python@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
  - `@trigger.dev/sdk@4.5.11`
  - `@trigger.dev/build@4.5.11`
## @trigger.dev/react-hooks@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/redis-worker@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/rsc@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/schema-to-json@4.5.11

### Patch Changes

- Updated dependencies:
  - `@trigger.dev/core@4.5.11`
## @trigger.dev/sdk@4.5.11

### Patch Changes

- Chat in the browser now reconnects when the connection drops mid-turn,
instead of leaving the reply stuck as if it were still generating.
Reports can be fetched as structured data with the `json` format, and
the shortest report period is now one minute (`1m`, `30m`, `1h`, `7d`).
The `mint-token` command's help is clearer too: a token minted without
`--cap` is read-only, and `--ttl` shows the correct maximum lifetime of
7 days.
([#4418](#4418))
- Watch-mode chat streams now survive quiet windows and page reloads,
and a reply cut off by a lost connection shows an error instead of
appearing finished. Aborting a resumed subscription only closes your
local stream — call `stopGeneration(chatId)` or pass `stopOnAbort: true`
to stop the run. Also fixed a race where quickly restarting a stream
could break stop and reconnect, and stopping a chat now hands it back to
your other tabs instead of leaving them read-only.
([#4516](#4516))
- Updated dependencies:
  - `@trigger.dev/core@4.5.11`

</details>

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants