Repository navigation
Conversation
🎩 PreviewA preview build has been created at: |
The New Project dialog and the /projects prompt box each get a workspace picker. Retired workspaces are left out of it. The preselected workspace is the one last created in, if it is still active, otherwise the oldest active one. The API lists workspaces by name, so "first listed" would move every user's default whenever a workspace that sorts earlier is added. Debug in Tangent keeps working in the run's own project, and falls back to the same default when the run has none. Project cards now name their workspace. A refused create now says why (e.g. a workspace retired since the list was fetched) and refetches the workspace list, and the prompt box says whether it is waiting on a backend, failed to load workspaces, or has none to offer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The function and test names carry the intent, and the reasoning is in the pull request description. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Tangent on, /projects renders the prompt box above the projects grid, and both said why a project could not be created. The grid now leaves the "no workspace" and "could not load workspaces" notices to the prompt box. The section still shows when there is no backend. The prompt box now creates its project through useCreateProject instead of a mutation of its own, so the refetch after a failed create and the error toast live in one place. workspace_changed is captured when the prompt is sent, so a project created in another tab mid-request no longer flips it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A Tangent project page gets a Secrets window in its left dock, listing the secrets of the workspace the project belongs to. Anyone the backend allows (can_create) can add one; the creator or an admin (can_manage) can replace its value or delete it, after a confirmation. Values are write-only and never shown. An expiry date can be set when adding or replacing, and is not shown in the list. The endpoints are not in the generated client yet, so the service calls the hey-api client by hand, as projectRunsService does. Regenerating sdk.gen.ts replaces the whole client from whatever backend is running, so that waits for the backend to merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scope line now reads "Shared with every project in ⬡ <workspace>.", with the Shapes icon labelled "Workspace" as on the project cards and the name in semibold, truncated when it is long. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2c01d7b to
e4156a4
Compare
22a2d21 to
c5366ed
Compare
b00e88c to
1305c77
Compare
| failure: string, | ||
| result: { error: unknown; response: Response }, | ||
| ): WorkspaceSecretsApiError { | ||
| const reason = describeErrorBody(result.error); |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
[P2] Redact submitted secret values from validation errors
describeErrorBody() includes each validation issue's raw input in the message. A 422 response containing detail[].input for secret_value therefore puts the submitted secret into this error, and useWorkspaceSecretMutation displays it in a toast. I reproduced this with a targeted service test: the returned error contained the submitted value in plaintext.
Use a redacted error formatter for secret requests that preserves the field and explanation but omits input values, including model-level inputs that could contain the whole request body. Add a regression test asserting that the submitted secret never appears in the error message.
75b10ef to
0dc0caa
Compare
Four fixes to the add and replace form, carried over from the superseded workspace-secrets draft (#2886). They apply to Settings → Secrets today and to the team secrets screens that follow. - The value field sets `autocomplete="new-password"`, so browsers stop offering a saved login password for it. - A name containing `/` is refused. The name is a path segment in the replace and delete requests, so such a secret could be created but never changed or removed. - Deleting asks first. A deleted secret makes every run that uses it fail at launch, and once secrets are shared with a team that is everyone's runs. - Description and expiry fields, using the expiry picker from #2886. The backend only writes either when a value is sent, so neither can be cleared once set: replacing keeps the current description and expiry unless a new one is given, and the form says so. Expiry is not enforced by the backend, so the form calls it a reminder. The Settings list shows a secret's description under its name. The E2E `removeSecret` helper now confirms the delete dialog. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tangent's project dock gains a Secrets window. It lists the secrets that runs submitted in the project use: the team's, when the project's workspace maps to a team you can act as, and your own otherwise. - In a team's project the window is headed "Team: <workspace>" with the team id as its tooltip, and add, replace and delete act as the team. A member whose role can't write sees the list without those actions. Hiding them is the only guard, since the backend doesn't check write access on `/api/secrets/`. - Everywhere else it is headed "My secrets". - When the project has a team but you can't act as it (not a member, refused by the backend, or the lookup failed), the window says so and lists your own secrets, because that is who your runs there will be submitted as. `ActingTeamNotice` holds that copy so the submit dialog can say the same thing later. - Projects on different teams list nothing and say why; submitting is blocked in that case. The add, replace and delete controls take an optional team id and invalidate that team's cache key. Their failure toasts now show the backend's reason, which matters once a request can be refused for acting as a team. From #2886: the dock registration and its order test, `ListRowIcon`, and the secret row's look. Replace and delete are inline buttons rather than a menu, so the row reuses the confirming delete button. Settings → Secrets stays your own secrets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(secrets): harden the secret form Five fixes to the add and replace form, carried over from the superseded workspace-secrets draft (#2886). They apply to Settings → Secrets today and to the team secrets screens that follow. - The value field sets `autocomplete="new-password"`, so browsers stop offering a saved login password for it. - A name containing `/`, or a name that is `.` or `..`, is refused. The name is a path segment in the replace and delete requests, and the URL parser collapses dot segments, so such a secret could be created but never changed or removed. - Name and description stop at 255 characters, the size of their backend columns, and say so when the limit is reached. Longer values failed with a bare "Failed to add secret" or were cut off. - Deleting asks first, with a "Delete" button. A deleted secret makes every run that uses it fail at launch, and once secrets are shared with a team that is everyone's runs. - Description and expiry fields, using the expiry picker from #2886. The backend only writes either when a value is sent, so neither can be cleared once set: replacing keeps the current description and expiry unless a new one is given, and the form says so. Expiry is not enforced by the backend, so the form calls it a reminder. The Settings list shows a secret's description under its name. The E2E `removeSecret` helper now confirms the delete dialog, found by its title. The React Compiler covers the new `ExpiryDatePicker`. Tests get one `ResizeObserver` stub in `vitest-setup.js` instead of a copy per suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(secrets): keep a secret's expiry on the day chosen An expiry chosen as Dec 25 came back as Dec 26. The form sent the end of the day as 23:59:59.999 UTC, and a database that stores datetimes to the second rounds that up to midnight. The form now sends the day's last whole second. The hint under a new secret's expiry is gone. Replacing a secret that already has one still says which date it keeps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(tangent): manage a project's secrets in a Secrets window Tangent's project dock gains a Secrets window when the project's workspace maps to a team. It lists the workspace's secrets, the ones runs in the project use when they act as the team. Your own secrets stay in Settings → Secrets. The copy calls the team the workspace. A team takes its workspace's name, and the workspace is what people know it by. - In a team's project the window is headed "Workspace: <workspace>" with the team id as its tooltip, and add, replace and delete act as the team. A member whose role can't write sees the list without those actions. Hiding them is the only guard, since the backend doesn't check write access on `/api/secrets/`. - A project whose workspace has no team has no workspace secrets, so it gets no Secrets window. The window opens once the team is known, so it never appears and then vanishes. - When the project has a team but you can't act as it (not a member, refused by the backend, or the lookup failed), the window says the workspace's secrets aren't available and why. It doesn't list your own secrets in their place, where they would pass for the workspace's. The add, replace and delete controls take an optional team id and invalidate that team's cache key. Their failure toasts now show the backend's reason, which matters once a request can be refused for acting as a team. From #2886: the dock registration and its order test, `ListRowIcon`, and the secret row's look. Replace and delete are inline buttons rather than a menu, so the row reuses the confirming delete button. Settings → Secrets stays your own secrets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(tangent): name workspaces with their icon in the Secrets window Feedback from the first staging pass. - A team's name, which is its workspace's, now carries the workspace icon wherever it's written into copy, with the team id on hover. `TeamName` does this, and the add dialog, the delete confirmation and the window's notice use it. Its 12px icon sits 0.1em below the baseline, which centres it on the capitals of both 12px and 14px text. `bold` sets the name in semibold and the icon at stroke 3, the weight that matches. The React Compiler covers `src/components/shared/Teams`, where it lives. - Deleting a workspace secret asks 'Delete shared secret "<name>"?' and says it's removed for the whole workspace, named in bold: "This removes the secret from the <workspace> workspace. Runs that use it will fail. This can't be undone." `ConfirmationDialog` now takes any node as its description, so the name carries its icon there too. - Adding a secret is titled "Add a workspace secret". - The replace dialog shows the secret's key icon at its top left, like the secret's row. - The empty-state line under the list is gone. The add button is enough. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Description
Stacked on #2881. Adds a Secrets window to the left dock of the Tangent project page. It manages the secrets of the workspace the project belongs to, so any run in any project of that workspace can use them.
What you can do
See the secrets. Each row shows the name, plus either the description or "Added by … · updated …". A line at the top names the workspace: "Shared with every project in ML Research."
Add a secret, when the backend's
can_createis true. The fields are:The list doesn't show expiry.
Replace a value or delete a secret, from a row menu that appears only where
can_manageis true:Failures carry the backend's
detail, e.g. "Failed to add secret: Secret 'API_KEY' already exists in this workspace."Where it goes
tangentProjectWindowOrder.test.tscovers that.Talking to the backend
GET/POST /api/workspaces/{workspace_id}/secrets/andPUT/DELETE …/{secret_name}, following the API contract.sdk.gen.tsyet, soworkspaceSecretsService.tscalls the hey-apiclientdirectly, asprojectRunsService.tsdoes. Regenerating replaces the whole client from whatever backend happens to be running, so that waits until the backend merges.can_createandcan_manage. The client never works them out itself.autoComplete="new-password". With a name field above a password field, browsers would otherwise offer to fill in a saved login password.Reuse
src/components/WorkspaceSecrets/, so the editor's secret picker in a later PR can use them.WindowListRowmoved intoListRowIcon. A secret row isn't a button (there's nothing to open), but it should look the same.To confirm once the backend is up
secret_name,descriptionandexpires_atas query params. This PR sends them in a JSON body, following the brief.new Date(). If the backend sends them without a timezone, as/api/secrets/does,parseAsUtcfromsecretsStorage.tsshould move tosrc/utils/date.tsand be used here too."". Does the backend accept that as a clear?PUTrequiressecret_value, so fixing only a description means typing the value again.Related Issue and Pull requests
Stacked on #2881 (workspace picker). Out of scope here, each a separate PR:
workspace_secretarguments;/adminpanel in oasis-frontend.Type of Change
Checklist
Screenshots (if applicable)
None yet: the backend endpoints don't exist yet.
Test Instructions
:8000. In.env, setVITE_BACKEND_API_URL=http://localhost:8000andVITE_ENABLE_TANGENT=true, then turn on the Projects and Tangent flags in Settings.Unit tests cover the service, the hooks, the dialog, the window and the dock order:
pnpm test.Additional Comments
Test vs production code
About 54% of the net lines are tests: 1021 net lines of tests against 866 of production code.
Per file
WorkspaceSecretDialog.tsx(new)workspaceSecretsService.ts(new)SecretsWindowContent.tsx(new)SecretRow.tsx(new)ExpiryDatePicker.tsx(new)useWorkspaceSecrets.ts(new)useDeleteWorkspaceSecretAction.tsx(new)types.tsListRowIcon.tsx(new)useTangentProjectWindows.tsxerrors.tsWindowListRow.tsxreact-compiler.config.jsworkspaceSecretEvents.ts(new)tangentProjectWindowOrder.tsWorkspaceSecretDialog.test.tsx(new)SecretsWindowContent.test.tsx(new)useWorkspaceSecrets.test.tsx(new)workspaceSecretsService.test.ts(new)tangentProjectWindowOrder.test.ts🤖 Generated with Claude Code