Skip to content

v2: surface UI — events, workflow triggers, team ownership, shared parts - #216

Open
xBalbinus wants to merge 15 commits into
dev-v2from
feat/v2-surface-ui
Open

v2: surface UI — events, workflow triggers, team ownership, shared parts#216
xBalbinus wants to merge 15 commits into
dev-v2from
feat/v2-surface-ui

Conversation

@xBalbinus

@xBalbinus xBalbinus commented Aug 11, 2026

Copy link
Copy Markdown

Summary

One PR for the V2 surface gaps — concepts that had full APIs and no UI — consolidating #214 and #215, plus a componentization pass so the new panels share one set of parts instead of each hand-rolling their own.

Events (/events, new top-level page):

  • Activity: the org's ingested events, filterable by service/event key from the plugin trigger catalog; a row expands into the raw payload and its delivery attempts, so "why didn't my trigger fire" is answerable in-product. Shows a notice when at the server's 50-event page size instead of silently looking complete.
  • Subscriptions: create/enable/disable/delete the rules that turn a matching event into a workflow run or an orchestrator prompt. The create dialog offers only catalog keys. A personal subscription is only mutable by its creator (enforced server-side; the UI hides/disables what the API would 404). Filters stay API-only for now.

Workflow triggers (editor → Triggers drawer):

  • Webhook: mint, copy, rotate, delete the trigger URL (rotate/delete confirm — the URL carries the bearer secret). The URL is built server-side and returned on the wire; the client renders it verbatim instead of guessing the origin.
  • Schedules: new routes GET/POST /api/workflows/:id/schedules and DELETE /api/workflows/:id/schedules/:scheduleId over the existing schedule service. Deleting the workflow deletes its schedules too. Delete confirms, like every other destructive control here.

Teams:

  • Owner picker in the New-workflow dialog and the skill editor (both now share one component); team-name badges on the workflows list.
  • TeamSummary.callerRole; the teams settings panel hides mutation controls the API's canMutateTeam gate would 404 anyway. No enforcement semantics changed.

Shared components (new, used across all of the above): ConfirmDialog, LoadingRow/ErrorRow/EmptyRow, TabBar (full ARIA tabs keyboard contract — arrow keys, Home/End, roving tabindex), SelectMenu (the trigger-button-plus-option-list pattern three different filters/pickers were each reinventing), OwnerPicker (caller-or-team select, filtered to teams the caller can actually create against), useCopyToClipboard, ~/lib/format-when, ~/lib/error-text. Panels are split into focused files rather than one growing god-component: event row/detail, subscription create dialog, webhook and schedule sections.

Dev tooling: packages/api/scripts/dev-seed-linear.ts + dev-events-smoke.ts drive the full webhook → event → subscription → run pipeline against a local stack.

Fixed after the first pass

An internal review pass caught 15 real issues in the initial diff — API validation gaps (a schedule accepted a whitespace-only name and a non-object input), an authorization gap (any org member could mutate a colleague's personal event subscription through the org-scoped routes), missing error surfaces on four different destructive/mutating controls, an orphaned-schedule bug on workflow delete, a duplicated webhook-URL builder, and the reuse gaps the componentization above closes. All fixed in this branch; see individual commits.

Verification

API (1979) and web (612) suites pass; full workspace typecheck clean. Exercised live end to end on the dev stack: created a team-owned workflow through the fixed OwnerPicker, opened its Triggers drawer, minted a webhook (confirmed the url round-trips), rotated it (confirmed the cache updates immediately, not after a refetch), created and deleted a schedule through its confirm dialog, and confirmed the DELETE requests land.

Brand refresh (2026-08-11)

Retargeted the design-token layer onto a real brand identity — decision record: docs/specs/2026-08-11-brand-refresh-design.md. One neutral system (cool OKLCH gray, replacing a separate warm palette), one accent (blue, sampled from the mascot illustration's uniform — extended to a full 50–900 scale), the unused serif heading face dropped for the sans stack it was already silently falling back to, wash colors rebuilt in native oklch(... / alpha) so they can't drift from their base color, plus a real favicon and theme-color meta tags. Two files changed the whole app's palette; verified live in light and dark across Events, the workflow editor, and Teams settings.

Codebase cleanup + agent-native run status (2026-08-11)

Full audit pass: zero orphan component files found, but four duplicate date-formatting functions and three hand-rolled clipboard-copy handlers had crept back in around this PR's own extractions — consolidated onto lib/format-when.ts / lib/use-copy.ts (the clipboard hook picked up an execCommand fallback one of the three copies had and the others didn't, so nothing lost capability).

Split the 522-line node inspector (editor/inspector.tsx) into editor/node-forms/ — one file per node type plus a registry dispatcher, same pattern the session tool-renderers already use. No behavior change; same 13 tests pass.

Two components pulled from the referenced "AI-native interfaces" gallery, scoped to what Valet's workflow surface actually needed: the run-detail checkpoint list now reads as color/glyph-coded status rows (same vocabulary the canvas already uses) instead of plain text, with a failed node's result auto-expanded and everything else collapsed; the pending-approval card gets real visual weight (accent-tinted, "Waiting on you" framing) instead of a plain bordered box.

Editor toolbar: demoted "History" (version restore — the one setup-once, rarely-needed action of the four) into an overflow menu, cutting the primary row to Runs / Triggers / Run.

Component patterns + team-oriented surfaces (2026-08-12)

Two follow-ups against the reference component gallery and the team-ownership work above.

Component patterns: ThinkingengineToWireParts previously dropped the model's thinking parts; now forwarded through the wire and rendered as a collapsed-by-default disclosure in the transcript. CodeBlock — one shared syntax-highlighted renderer for markdown-fenced code, replacing the ad-hoc <pre> treatment markdown.tsx had; selectively registers only the languages this product renders (shell, TS/JS/TSX/JSX, JSON, diff, YAML, SQL, Markdown, Python) instead of Prism's full ~290-language bundle, and colors tokens from the app's own light/dark CSS variables rather than a fixed prebuilt theme. Elapsed-time counter on the agent status badge, sourced from the server-stamped turn-start event rather than the client clock.

Two other reference patterns were scoped out for lack of backend support: Streaming Text's inline sources/follow-ups, and the Prompt Bar's @-mentions/slash-commands/dictation. Both need server-side data this API doesn't produce yet.

Team-oriented resources: skill sources can now be imported as team-owned (same OwnerPicker the workflow/skill editors already use), with a badge on the row showing which team owns it.

Team orchestrators were structurally real already — workflow dispatch could wake one — but nothing let a member view it: nothing created its app-row on first access, and the session read routes only checked direct ownership. Adds POST /api/teams/:id/orchestrator (get-or-create, reusing the existing ensureOrchestratorSession helper) and widens GET /api/sessions/:id, the messages/threads/decisions routes, and the WS handshake to also allow a caller who can view the owning team. Read-only and team-scoped — no change to mutating session routes, no org-level access. TeamsPanel gets an "Assistant" button, visible to any member, that opens the team's orchestrator session.

Also fixed: createWorkflowSchedule always wrote ownerType: "user" regardless of who actually owned the workflow being scheduled — now derives it from the workflow's real owner (team ownership maps to org; the schedule-owner enum has no team value).

Verification (2026-08-12)

API (1993 passing, 30 pre-existing skips) and web (644) suites pass; full workspace typecheck clean. Live-verified on the dev stack in both light and dark: CodeBlock's syntax highlighting end to end through a real chat turn, and the team-orchestrator flow — clicking Assistant creates the session and navigates to it, the session renders through the widened read routes, and a second click returns the same session (get-or-create is idempotent).

Team surfaces moved out of Settings (2026-08-12)

A team's assistant was reachable only at Settings › Organization › Teams, on a small ghost button, behind an org-admin gate most members fail. Settings is where you configure things; that is the wrong place to keep a conversation.

Each team you belong to now has a permanent row in the /chat sidebar, beside your own assistant, addressed by /chat?team=<id>. Finding your team's assistant is the same act as finding your own — two clicks for any member. This follows Notion's teamspace pattern rather than a switcher or a separate destination: membership is navigation, not a mode, so no view re-scopes and every surface stays as cross-team as it was.

The session id is derived client-side from the engine's well-known format, so browsing the rail creates nothing — /chat ensures the session only when someone opens the conversation. A strip above the composer names how many people can read what you are about to type.

Supporting changes:

  • Dashboard "Your teams" card. The chat sidebar is collapsible and remembers it, so it cannot be the only place teams appear.
  • Owner badges become links. Workflows, skill cards, skill docs and skill repositories each resolved the owning team's name for themselves — four copies, four looks, and the skill card never resolved it at all (it rendered the literal word "Team", a bug skill-doc and the skill detail page inherited through the same helper). Now one OwnerBadge, linking into that team's assistant, so flat lists become a second way in.
  • Settings keeps admin CRUD. Its Assistant control stays, as a plain link into /chat rather than a competing door. The org-admin gate is unchanged.
  • Usage card's "Team" heading is now "Organization" — it always listed the whole org roster.

Nothing renders for a caller with no teams: the rail block, the dashboard card, and the badges all require the organizations feature on and at least one real membership. A solo user's app is byte-identical to before.

Two bugs this surfaced

The header lied. It derived "is an orchestrator" from a bare id.startsWith("orchestrator:") and then titled the session from the viewer's own assistant, so every member saw their personal assistant's name on a shared team conversation. It now resolves the team and marks the session as shared.

Lifecycle authorization ignored the team. PATCH /api/sessions/:id, POST /:id/pause and DELETE /:id all filtered on agent_sessions.userId, which on a team-owned row records whoever opened the assistant first. That member could pause or delete an agent the whole team shares; everyone else got a 404. Adds canAdministerSession (team-owned → team admin or org admin) as the deliberate mirror of canViewSession, and extracts canAdministerTeam so the team mutation routes and session administration share one definition. POST /:id/auto-title had the same filter, which left a member's threads permanently untitled — titling is part of prompting, so it moves to canViewSession.

Known gaps

A team assistant has no chosen name, personality, presence dot, or nested child sessions, because GET /api/orchestrator/info and /children resolve the caller's own principal. It carries the team's name from GET /api/teams and its threads work fully. Nested children are deliberately omitted in team scope rather than shown wrong.

Verification

API (2014 passing, 30 pre-existing skips) and web (674) suites pass; workspace typecheck clean. Exercised live on the dev stack in light and dark: opened a team assistant from the rail, the dashboard card, and a workflow's owner badge; confirmed the shared-with strip, the corrected title, the thread list switching scope, and that an unknown ?team= falls back to the personal assistant with a notice.

@valet-valet-turnkey-dev valet-valet-turnkey-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The PR is in good shape overall — the new API routes, wire types, query hooks, and UI components are internally consistent and well-tested. Two correctness issues are worth fixing before merge.

  1. packages/api/src/routes/workflows.ts:~316 — deleteWorkflowSchedule returns 200 whether the schedule existed or not, but the existence check above already verified it; the concern is the opposite direction: if deleteWorkflowSchedule internally no-ops (e.g. concurrent delete), the response still says deleted: true. More critically: after the belongs-to-workflow list check passes, deleteWorkflowSchedule is called with only owner.orgId and scheduleId — it does not re-verify the schedule belongs to this workflow at the service level. The guard is a list-then-delete with no lock: a TOCTOU window exists where a concurrent request could reassign or re-use the scheduleId between the listWorkflowSchedules read and the deleteWorkflowSchedule write. This is only exploitable if the service allows schedule IDs to be reused or transferred, which may be unlikely in practice, but the route should pass workflowId into the delete call (or the service should accept it) so the WHERE clause is scheduleId = ? AND workflowId = ? instead of just scheduleId = ?.

    • Consider adding workflowId to deleteWorkflowSchedule's signature/query to make the constraint atomic.
  2. packages/web/src/components/workflows/webhook-section.tsx:~34 — window.location.origin is used to construct the webhook URL displayed to users. In environments where the web app is proxied or served on a different origin than the API (common in dev and some production setups), this will silently produce a URL that doesn't work. The API host should come from the same configuration source as api/client.ts's API base URL, not from window.location.origin.

  3. packages/web/src/components/settings/teams-panel.test.tsx:~56-58 — the test suite re-renders into the same container across it blocks without a beforeEach(() => { document.body.innerHTML = ''; }) or cleanup(). The callerRole/orgRole mutation-between-tests pattern combined with no DOM cleanup means earlier renders can bleed into later assertions (screen.queryByRole searches the entire document). This may not manifest today because the mocked data drives callerRole before render, but it's fragile — a failed assertion can leave stale DOM. Use @testing-library/react's cleanup (automatic with vitest-jsdom when configured, but worth verifying it's wired).


Created on behalf of Xiangan He xiangan@turnkey.io

- Validate schedule name (trim, reject blank) and input (must be an
  object) instead of persisting junk into every scheduled run's trigger
  payload.
- Delete a workflow's schedules when the workflow itself is deleted,
  matching the existing webhook cleanup.
- Scope the schedule DELETE route's ownership check to a WHERE predicate
  instead of an org-wide scan-and-.some().
- Dedupe the webhook URL builder (webhookUrl in actions.ts duplicated
  workflowWebhookUrl in webhook-service.ts); the HTTP routes now return
  the same absolute url the agent-facing tool already computed, with the
  request's own origin as a fallback when no public origin is configured.
PATCH/DELETE on a personal (user-owned) subscription now 404 for any
caller but its creator, matching the cross-owner "not found" convention
used elsewhere. Previously any org member could enable/disable or delete
a colleague's personal subscription through the org-scoped routes. An
org-owned subscription stays mutable by any org member.
dev-seed-linear.ts passed PGlite straight to PgCredentialStore, whose
PgQueryable contract requires query() to return rowCount — PGlite's
result type doesn't have one, so this broke the repo's `tsc --build`.
Adapts through a thin query wrapper instead.

dev-events-smoke.ts probed three guessed shapes for the webhook mint
response (hookId/secret/parsed-from-url); imports the real
WorkflowWebhookResponse type instead.
New reusable pieces, extracted after the events+triggers+teams panels
independently reinvented the same patterns:

- OwnerPicker: the caller-or-team select used by the workflow and skill
  editors. Filters to teams the caller can see AND is a member of
  (callerRole !== null) — the prior two copies each offered every team an
  org admin's useTeams() returns, including ones they aren't a member of,
  which the create routes then 404 on.
- SelectMenu: the trigger-button-plus-option-list pattern behind the
  event feed's service/key filters and the subscription dialog's
  workflow picker (three near-identical DropdownMenu blocks before this).
- useCopyToClipboard: extracted from tool-shell's CopyButton (which now
  uses it too) so the workflow webhook section doesn't hand-roll its own
  copy-state timer, and so both handle a denied/unavailable clipboard.
- TabBar: adds the ARIA tabs keyboard contract (arrow keys move focus and
  selection with roving tabindex, Home/End jump to the ends) and an
  aria-controls/tabPanelId pairing for consumers to wire a real tabpanel.
- new-workflow-dialog and skill-editor now share OwnerPicker instead of
  each keeping its own copy-pasted <select>, and route their create
  errors through errorText instead of showing raw error.message.
- Webhook rotate/delete: the ConfirmDialogs now receive the mutation
  error (previously invisible, rendered below the modal overlay) and
  the initial "Create webhook URL" failure has its own error line.
  useMintWorkflowWebhook seeds the query cache from its response so a
  rotate doesn't keep showing the just-revoked URL until the refetch
  lands; useDeleteWorkflowWebhook invalidates onSettled so a failed
  delete still reconciles.
- Schedule delete now confirms (it was one click, unlike every other
  destructive control added this pass) and surfaces its error; the
  empty-state message no longer renders alongside the error message on
  a failed fetch; useDeleteWorkflowSchedule invalidates onSettled.
mutations by owner, close remaining error/reset gaps

- Feed's two filter dropdowns and the create dialog's workflow picker
  now use SelectMenu instead of three copies of the same DropdownMenu
  block. Added a truncation notice when the feed is at the server's
  50-event page size (no pagination yet) instead of silently looking
  complete. The load-failure message now names the Refresh button as
  the corrective action.
- listEvents built its query string with a URLSearchParams.size check —
  unsupported by some engines, which would silently drop the
  service/key filters; switched to .toString().
- The subscriptions list disables the enable/disable switch and hides
  the actions menu for a subscription the caller isn't allowed to
  mutate (mirrors the new server-side gate), and the switch surfaces a
  failed PATCH instead of silently snapping back.
- The create dialog now resets its form and error state on any close
  path (Cancel, overlay, Escape), not just a successful submit — it
  stays mounted between opens.
- teams-panel: dropped a dead Spinner import and routed its two raw
  error.message displays through errorText.

Also adds the ARIA tabpanel half of the /events tab strip (role,
aria-labelledby) to match TabBar's new aria-controls wiring.
@xBalbinus

Copy link
Copy Markdown
Author

1. Schedule delete scoping — partly acted on, in 1ae0474a.

The cited concern is already closed at HEAD: deleteWorkflowSchedule takes workflowId and the route passes it (routes/workflows.ts:339), so the service adds eq(workflowSchedules.workflowId, workflowId) to its predicate rather than trusting a prior list call. A regression test asserts a cross-workflow delete returns 404.

There was a real residue one line below the cited spot, though: the check was scoped but the DELETE still filtered on id alone (schedule-service.ts:167). That made the scoping correct only by luck — ids are UUID primary keys and nothing in the codebase reassigns workflow_id or org_id, so the two statements cannot resolve to different rows today. The delete now runs under the same conditions as the check, which makes it a property of the statement instead of an invariant a later change could break without touching this file.

The route guard is owner/team scoped, which is strictly stronger than the org scope described.

2. Webhook URL from window.location.origin — not present. webhook-section.tsx renders hook?.url, built server-side by workflowWebhookUrl from VALET_PUBLIC_URL with the API's own request origin as fallback — the configured source the finding asks for. The reviewed line came from an intermediate commit on the branch, not HEAD.

3. DOM cleanup in teams-panel.test.tsx — already wired. src/test/setup.ts registers an afterEach that calls Testing Library's cleanup, loaded via setupFiles in vitest.config.ts. Confirmed load-bearing rather than incidental: dropping setupFiles makes the third case fail on a duplicate match. Both files pre-date this branch and are untouched here.

Full api suite green (1979 passed).

…ent-native run status

Splits the 522-line node inspector into one form file per node type
(mirrors the tool-renderer registry pattern). Consolidates four duplicate
date formatters onto lib/format-when.ts and three hand-rolled clipboard
handlers onto lib/use-copy.ts (upgraded with the execCommand fallback one
of them had, so nothing regresses). Upgrades the run-detail checkpoint
list from a plain-text status list to color/glyph-coded rows sharing the
canvas's own NodeRunStatus vocabulary, with failed nodes auto-expanding
their result and everything else collapsed by default. Gives the pending-
approval card real visual prominence instead of a plain bordered box.
Demotes the editor toolbar's least-used action (version history) into an
overflow menu, cutting the primary row from 4 buttons to 3.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant