Skip to content

feat: add pipelines held in this browser to a project - #2745

Draft
camielvs wants to merge 8 commits into
09-17-feat_project_pagefrom
09-18-feat_local_pipelines_in_projects
Draft

camielvs wants to merge 8 commits into
09-17-feat_project_pagefrom
09-18-feat_local_pipelines_in_projects

Conversation

@camielvs

@camielvs camielvs commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Description

A project lives on the backend. A pipeline lives in one browser's IndexedDB. Until now the Pipeline item in a project's + Add menu was disabled, because the only pipeline the resources API accepts is one the backend already holds — and on this branch there is no way to put one there. So the one thing a user can actually make was the one thing they could not add.

This branch closes that gap as things actually are: a project row points at a pipeline in the browser that made it. Nothing is copied.

The alternative was a snapshot of the YAML, which would be shareable but would drift from the pipeline it was taken from the moment anyone edited it. Drift is the worse failure, so a pointer it is — and the page says plainly what that costs rather than pretending.

The row

The entity enum is closed (pipeline | agent_session | document) and a pipeline must name a backend pipeline by UUID. A made-up UUID is accepted by the API, but it would sit in a shared table looking exactly like a real backend pipeline id to every other client and to agents, which would then fail to fetch it. So the row is filed as the only thing it honestly is — one carrying no content of its own — and extra_data records what it names:

entity:     "document"
name:       <pipeline name when it was added>
payload:    {}
extra_data: { kind: "pipeline", storage: "browser", localName, localId? }

The pointer has to live in extra_data, not payload: the list endpoint omits payloads entirely, so a list that needed to know what each row held would cost one request per row.

Resolution: the id first, then the name

The id is tried before the name, which is the opposite of what seems natural. A pipeline's content is keyed by name, and the name finds every pipeline while the id only exists for those the newer editor has touched — so name-first looks right. It is wrong on one ordinary sequence:

  1. add "training" to a project
  2. rename it to "training-v1"
  3. call the next pipeline "training"

By name, the project now resolves to a different pipeline, and looks perfectly successful doing it. Registry ids are never reused, so an id that still resolves is the pipeline that was added. The name stays as the fallback, and a pointer that resolves by neither reports itself as unresolved rather than guessing.

The id is read, never created. Creating one would enter the pipeline into the registry the newer editor builds its folder listing from, so adding a pipeline to a project would quietly change what another page shows.

What this costs, stated plainly

  • A pointer is not shareable. Another member — or the same person in another browser — sees a row that cannot be resolved, and the panel says so and says who added it, rather than rendering as broken.
  • It can vanish. Delete the pipeline and the row dangles, where a copy would have survived. That is the same coin as never drifting.
  • A rename through the older editor breaks it. That path renames content without updating the registry, so both lookups miss. The row then reports itself unresolved rather than resolving wrongly.
  • A name can be recycled with no id. A pipeline that never got a registry row, renamed away, with a new pipeline taking its old name, resolves to the new one. There is no content-stable identity available to prevent this — the digest is recomputed on every write.
  • These rows count as documents everywhere counts are shown, because the API knows them only by entity and the /projects cards read resource_counts straight off it. The project page counts the same way rather than contradicting the card, so a browser-held pipeline sits under Documents with a Local pipeline badge. Honest, but it does mean the group heading is not the whole truth about its rows.

Worth raising separately: a pipeline_ref member in the entity enum would remove the document fiction entirely, fix ?entity= filtering and the card counts, and costs an enum member plus a PAYLOAD_ENTITIES entry.

Also here

  • A document says what kind it is. Any row whose extra_data.kind names something its group heading does not gets that kind as a badge beside its name — Local pipeline here, but a data_sheet document would read Data sheet without the table learning the word. A row with no kind, or one that only repeats its entity, stays bare.

  • Removing one no longer offers to delete it. removingDestroys treated "carries no entity_id" as "holds the only copy", which would have given these rows a bin and "deleting it here deletes it for good." It now asks the weaker question — does the row claim to name a pipeline — so even a row whose pointer has become unusable is still removed, not deleted.

  • The editor link for a backend pipeline is gone. The editor addresses a pipeline by name out of browser storage, so the link resolved a backend pipeline's name against the wrong store — opening a local namesake where one existed, an empty canvas where none did. Routing to a backend pipeline does not exist yet, so the preview says so instead.

  • Judging a spec no longer throws on one too malformed to have an implementation. A pipeline read from browser storage is whatever YAML is there, and declining to judge it beats breaking the panel it is shown in.

  • The picker mounts only while open, so opening a project no longer reads browser storage for a list nobody asked for.

Verification

npm run validate and the full suite (2761 tests) green. Unit tests cover the resolution order — including the recycled-name case above as an explicit regression test — the row encoding, the removal wording, the preview's two states, and the picker.

Driven in a real browser against a live backend, in both themes: a pipeline imported through the app's own import path, added through the picker, previewed as its stored YAML with a validity verdict and a working editor link; offered but disabled on reopening the picker; removed with an X while a document keeps its bin; the page's group counts read back and checked against the card's summary for the same project; and a pointer naming a pipeline this browser does not hold rendering as "Not in this browser" with no editor link and no {} leaking from the empty payload.

🤖 Generated with Claude Code

@camielvs
camielvs requested a review from a team as a code owner September 18, 2026 18:20
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

🎩 Preview

A preview build has been created at: 09-18-feat_local_pipelines_in_projects/826eab1

@camielvs
camielvs force-pushed the 09-17-feat_project_page branch from d70b987 to 1e2f163 Compare September 18, 2026 21:19
@camielvs
camielvs force-pushed the 09-18-feat_local_pipelines_in_projects branch from 694dfab to b3ac3e8 Compare September 18, 2026 21:19
@camielvs
camielvs force-pushed the 09-17-feat_project_page branch from 1e2f163 to 501de46 Compare September 18, 2026 21:26
@camielvs
camielvs force-pushed the 09-18-feat_local_pipelines_in_projects branch from b3ac3e8 to b59af04 Compare September 18, 2026 21:26
@camielvs
camielvs force-pushed the 09-17-feat_project_page branch from 501de46 to 98b2313 Compare September 18, 2026 21:45
@camielvs
camielvs force-pushed the 09-18-feat_local_pipelines_in_projects branch from b59af04 to 3689e2f Compare September 18, 2026 21:45
@camielvs
camielvs force-pushed the 09-17-feat_project_page branch from 98b2313 to 74d9257 Compare September 18, 2026 22:09
@camielvs
camielvs force-pushed the 09-18-feat_local_pipelines_in_projects branch from 3689e2f to 53a3efb Compare September 18, 2026 22:09
@camielvs camielvs mentioned this pull request Sep 18, 2026

camielvs commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

camielvs and others added 8 commits September 23, 2026 13:53
… names

A project lives on the backend and a pipeline lives in one browser, and the
resources API will only accept a pipeline that the backend itself holds. So a
project records a browser-held pipeline as what it honestly is — a row with no
content of its own — and says in extra_data which pipeline it names. Nothing
is copied, so there is no second version to drift.

The id is tried before the name when resolving one. Renaming a pipeline frees
its name, and the next pipeline to take that name is a different pipeline; by
name alone that resolves to the wrong one and looks successful. Ids are never
reused, so an id that resolves is the pipeline that was added. The name stays
as the fallback, because most pipelines have no registry row at all.

Answering "does this browser still hold it" deserializes nothing: a list of
rows costs one pass over the stored names, not one decoded pipeline per row.
The API has no way to record a pipeline that only exists in a browser, so it
is stored as a row with no content of its own. That is the database's
business, not the reader's: someone looking for their pipelines should find it
under Pipelines, counted with them, whether it lives here or on the backend.

Removing one no longer offers to delete it. A row that names a pipeline has
never held one, so the icon stays an X and the confirmation says the pipeline
stays in this browser — and it keeps saying that even when the pointer has
become unusable, since a broken pointer still holds nothing worth destroying.
The pipeline is read where it lives, so the preview shows what the pipeline
says now rather than what it said when it was added, and it opens in the
editor under the name it has now — not the one it had then. Validity is
reported for it exactly as for a pipeline on the backend.

When the pipeline cannot be found, the panel says so and says who added it,
rather than rendering the row's empty payload as content — which the document
preview would otherwise show as the two characters "{}".

Judging a spec no longer fails on one too malformed to have an implementation.
A pipeline read out of browser storage is whatever yaml is there, and
declining to judge it beats breaking the panel it is shown in.

The editor link for a pipeline stored on the backend is gone: the editor opens
pipelines by name out of browser storage, so that link led to an empty canvas
and made the pipeline look lost.
The Pipeline item in the Add menu was disabled because nothing a user could
actually make was attachable: the API only takes a pipeline the backend holds,
and there is no way to put one there from here yet. It now opens a picker of
the pipelines in this browser and records the one that is chosen.

The picker says up front that a named pipeline cannot be shared, so nobody
discovers that later. A pipeline already in the project is shown but not
offered again — matched on what each existing row resolves to as well as the
name it was added under, so a renamed pipeline is not added twice.

The picker mounts only while it is open, so opening a project no longer reads
browser storage for a list nobody asked for.
Removing a row took a delete followed by a re-read of the whole list before
anything moved on screen — two round trips of nothing happening, which reads
as a click that did not land. The row now goes as soon as it is asked for, and
comes back if the request fails.

The refresh afterwards was also doing more than it needed. A project's key is
a prefix of both its resources and its runs, so invalidating it plainly
re-read the resource list a second time and the run feed as well, for a change
that cannot have touched either. Removing one item was four requests; it is
now two.
The tile reads `resource_counts` straight off the API, which knows a
browser-held pipeline only as a document, so the page saying "Pipelines
(2)" where the tile said "1 pipeline · 2 documents" put the two in
contradiction with no way for a reader to tell which was lying.

Group on the entity instead, and let each row say what it is with an
icon of its own, since a group headed "Documents" now holds rows that
are not all documents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The editor addresses a pipeline by name out of browser storage, so the
link resolved a backend pipeline's name against the wrong store: where a
local pipeline happened to share the name it opened that one instead,
and where none did it opened an empty canvas. Routing to a backend
pipeline does not exist yet, so the preview says so rather than offering
a way in that goes somewhere else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Marking a browser-held pipeline with the same icon a real pipeline
carries said the two were the same thing, one line apart in the same
table. A group heading names the entity the API files its rows under, so
what a row calls itself in `extra_data.kind` is said on the row as a
badge, where it reads as a qualifier rather than a claim to be something
else.

Any kind gets one, not just this one — a `data_sheet` document says so
without the table needing to know the word. A row with no kind, or one
that only repeats its own entity, stays bare.

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

This branch has not been deployed

No deployments
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