Skip to content

Feature: Start on workshop space template - #167

Merged
jhweir merged 3 commits into
devfrom
feat/workshop-space
Aug 27, 2026
Merged

Feature: Start on workshop space template#167
jhweir merged 3 commits into
devfrom
feat/workshop-space

Conversation

@jhweir

@jhweir jhweir commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Workshop — an MVP space built out of panels

Branch: feat/workshop-space off dev. Three commits, 20 files, +962/−24. The consumer half of the
panel-layout work merged in #166, and the first template in the repo that declares meta.panels.

This is an MVP. It is enough to have proved the panel-layout mechanism
against something real — which is what it was for. It is not a finished space. The sections marked
Deliberately thin and Known issues below are the honest list of what is missing, and most of it
is missing because the point of this PR was to exercise the layer beneath it rather than to build
out a product.

Summary

Workshop is the seventh showcase template, and the first about the surfaces around the content
rather than an arrangement of the content itself. The other six argue that channels, boards,
playlists and events are one container seen differently; this one argues that where the panels sit
is data too.

A call is running. Down the left, the transcript as everyone speaks, and beneath it a readout of what
extraction is making of it. On the right, the call. In the middle, a board of the records the call
produced — tasks and events — draggable and joinable. Two further routes read the same material
differently: tasks by state, and the record of past calls with their transcripts. The panels change
with the route
, which is the whole point: a board wants a transcript beside it and a task list does
not.

It mints no content model, like the six before it. The transcript is the collection
@we/module-transcribe already writes; the cards are the TaskBlocks and EventBlocks extraction
already makes from it.

Building it found two things the declaration could not express, one gap the previous PR left, and
three bugs — two of them in the shell rather than in the template.

Changes

What the first real layout needed

  • route on TemplatePanel — scopes a panel to a path segment. A section declaring its own
    meta.panels needs none of it, but a shell that routes itself has no sections to hang a
    declaration on. That is not a showcase-package property, incidentally: any template may own,
    delegate or mix its route table, and these six happen to own theirs.
  • open: false on TemplatePanel — places a panel without opening it. Opening a module panel
    means invoking the action its launcher declares, and that is not always "open a panel": the call
    module's is goToCall, which joins a call when there is not one. Without this, entering the
    space would have started a call.

The template

WorkshopTemplate.schema.ts, registered in the showcase index, the template registry CATALOGUE
and the seed — it appears in the picker as "Workshop" beside Channels and Timeline.

Three routes filling the screen behind a floating pill switcher, which is centred on the content
through --we-chrome-center-x so it does not drift when a panel displaces an edge, and which
declares meta.chromeReserve so floating panels clear it.

The board uses the board seed over the call's collection, narrowed with contains to what
extraction produces — the collection's children are also every utterance, and a board of six
hundred transcript fragments is not a board. manual layout parks unplaced records in a grid;
onNodeDragEnd writes drops back.

The extraction panel is template-authored — a node entry reading interpretationStore — which
is the proof that gap 3 of the plan works end to end.

Three bugs, found by using it

  • A template's panel never mounted. slotRegistry had no change channel and TemplateProvider
    read nodes() once. Every contribution until now arrived at boot, so reading once was enough for
    years; template panels register reactively, afterwards, into a list nobody read again. The dock
    resolved geometry for a panel that never rendered — which looks like the panel being broken rather
    than absent. The registry announces now, and the shell schema re-reads on it.

    The subtle half: the shell schema had no type, and a typeless node renders children through an
    unmemoized fragment, so a getter would never have been tracked. It needed type: 'Column' with
    display: contents to reach createMemo(() => renderChildren(node.children)). Because
    renderChildren maps with a reference-keyed <For> and the node's identity never changes, a new
    panel mounts alone and a running call's video stays put.

  • A declaration was silently outranked by unrelated history. Placements were keyed by dock id
    alone, so a transcript dragged while testing columns in the previous PR kept that position under
    every other template and beat whatever the next one declared. That is the three-rung chain working
    and scoped wrongly. Keyed by the declaring template now; templates that declare nothing keep the
    unscoped key, so a panel positioned in an ordinary space stays where it was put.

  • The board's placeholder sat under the switcher. The graph is meant to run under the floating
    bar; centred text in the same box is not, and reads as content that has slid underneath.

A gap the previous PR left

The schema validator did not know meta.chromeReserve or meta.panels and refused both outright —
the types knew and zodSchemas.ts did not. Caught the first time a template used them, which is the
check working. panels[].node is typed but not structurally recursed: this schema is what checks a
node, so recursing would be a cycle, and the validator's own traversal walks it anyway.

Deliberately thin

The MVP list. None of these is a bug; all of them are work not done.

  • The board only ever shows the call happening now. It is seeded from
    modules.transcribe.collectionId, which the module releases when the call ends — so the board
    empties. There is no picker for a past call's board, which is the first thing this needs to be
    useful beyond a live meeting, and the Record route already lists exactly the collections it would
    choose from.
  • The tasks route is read-only. Three status columns, author byline, due date. No editing, no
    assignment, no "who owes what" beyond who wrote it — the design for that was never settled, so this
    is a placeholder shaped like the eventual thing rather than the thing.
  • The extraction readout lists tasks only. The board draws TaskBlock and EventBlock; the
    readout queries TaskBlock. An extracted event appears on the board and not in the panel beside
    it. $query takes one entity, so the fix is two loops or a hoisted pair — deliberate rather than
    overlooked.
  • The Record route was my choice, not a specified one. The brief named the graph and a tasks
    view; a third was needed and past calls were the obvious candidate.
  • No manual "extract now" in the workshop — it relies on transcribe's own trigger.
  • EXTRACTED restates EXTRACT_CLASSES from transcribe, because templates → modules is a
    sideways edge. If that list grows, the board silently stops showing the new kind.

Known issues

  • The extraction readout appears twice during a call on the board route: once in the workshop's
    panel, once in transcribe's fixed chrome under the call bar. They are the same data — transcribe's
    runningPasses / settledPasses / hasActivity are thin filters over the same
    interpretation.activity() that interpretationStore exposes. This is what the plan's PR D
    resolves; it is a behavioural change to transcribe's call UX that wants its own manual testing.

  • Placement scoping is per template, not per (view, dock) as the plan specified. A panel dragged
    on one route keeps that position on the others within the same template. Enough to stop the bug
    that prompted it; not the whole of what was designed.

Known follow-ups

  • PR D, per above.
  • Per-route placement scoping.
  • A board picker for past calls — the single biggest thing between this and a usable space.
  • Auto-reveal titlebars, still wanting a mechanism for descendant styling.
  • The narrow-viewport column collapse still wants reconciling with mobile-plan.md.
  • slotRegistry gaining a change channel is the second registry to grow one by hand. That is the
    architecture plan's priority 4 (one registry primitive) arriving as a bill rather than a proposal.

Test plan

Run and passing:

  • @we/app-shell — 729 tests, including new coverage for slot-registry notification (register,
    remove, and staying quiet on a no-op removal) and for declaration scope.
  • @we/schema-shared — 615 tests; validate clean across 33 schemas including the new template;
    role-audit clean.
  • @we/primitives 138; @we/module-transcribe 80.
  • pnpm validate:seed clean; generate-templates lists 8 templates.
  • Full pnpm build clean with no tracked-file drift — the CI gate that caught PR Feature: Panel layouts transcript surface #166.

Verified by hand in the Electron app: the Workshop template appears in the picker, the board route
renders, and the panels resolve from the declaration. The three bugs above were all found this way,
which is the argument for having built the consumer at all.

Not yet verified, and wanting a real call with a second agent:

  • Every participant's lines in the transcript panel.
  • Extracted records appearing as board cards, and drops surviving a reload.
  • The call panel arriving on the right when a call starts, without the template having started it.

jhweir and others added 3 commits August 27, 2026 20:04
…e to be opened

Two things the first real layout needed and the declaration could not say.

`route` scopes a panel to a path segment. A section declaring its own
meta.panels needs none of it, but a shell that routes *itself* has no sections
to hang a declaration on — which is how every showcase template works, and is
what a workshop template built that way would hit immediately.

`open: false` places a panel without opening it, and it exists because opening
a module's panel means invoking the action its launcher declares — which is not
always "open a panel". The call module's is goToCall, and goToCall *joins a
call* when there is not one. A template that placed the call window would have
started a call on entering the space. The default stays "open", because a
declared panel nobody can see is pointless; the opt-out is for the case where
the module's own verb is bigger than the panel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…of it

The seventh showcase template, and the first about *panels* rather than a
content arrangement. The other six show that channels, boards, playlists and
events are one container seen differently; this one shows that where the
surfaces around the content sit is also data.

A call is running. Down the left the transcript, and beneath it a readout of
what extraction is making of it. On the right the call. In the middle a board
of the records the call produced — tasks and events — draggable and joinable.
Then two more routes over the same material: tasks by state, and the record of
past calls. The panels change with the route, which is the point: a board wants
a transcript beside it and a task list does not.

It mints nothing. The transcript is the collection module-transcribe already
writes, the cards are the TaskBlocks and EventBlocks extraction already makes
from it. The template is arrangement over both — the claim this package exists
to make, one level up from content.

Three of the four kinds of meta.panels entry are exercised: `module` for the
transcript and the call, `node` for the extraction readout (this template's own
schema, reading interpretationStore), `route` because a showcase template
routes itself and has no sections to hang a declaration on, and `open: false`
on the call so placing it never joins one.

The board seed narrows to what extraction produces — the collection's children
are also every utterance, and a board of six hundred transcript fragments is
not a board. `manual` layout parks anything unplaced in a grid, so a freshly
extracted record arrives somewhere sensible rather than stacked at the origin.

Also teaches the schema validator the two meta keys the last PR added: it
refused `chromeReserve` and `panels` outright, which is the gap working as
intended — the types knew about them and the validator did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e it was asked to

Three bugs the first real layout found, all in the layer that ships this.

**A template's panel never mounted.** slotRegistry had no change channel, and
TemplateProvider read nodes() once when it built the shell schema.
Contributions used to all arrive at boot — modules register before the first
render — so reading once was enough for years. A template declaring panels
registers its frames reactively, long afterwards, into a list nobody read
again: the dock resolved geometry for a panel that never mounted, which looks
exactly like the panel being broken rather than absent.

So the registry announces, and the shell schema re-reads on it. The node needs
a `type` for that to reach anything: a typeless node renders its children
through an unmemoized fragment, so the read is untracked and a getter would
never re-run, while a typed one goes through
`createMemo(() => renderChildren(node.children))`. `display: contents` keeps
the wrapper out of the layout — the same trick dockFrame uses. The node's own
identity never changes and renderChildren maps with a reference-keyed For, so a
new panel mounts alone and a call's live video stays where it was.

**A declaration was silently outranked by unrelated history.** Placements were
keyed by dock id alone, so a transcript dragged while trying out one template
kept that position under every other one and beat whatever the next template
declared for it. That is the three-rung chain working and scoped wrongly: a
drag is a fact about this panel *in this interface*. Keyed on the declaring
template now. An interface that declares nothing keeps the unscoped key, so a
panel somebody positioned in an ordinary space stays where they put it — the
scope exists to stop declarations being overruled by unrelated history, not to
make every template forget.

**The board's placeholder sat under the switcher.** The graph is meant to run
under the floating bar — that is what a full-bleed view with chrome over it
looks like, and its controls are at the other corners. Text centred in the same
box is not; it reads as content that has slid underneath something, because it
has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploy Preview for coasys-we ready!

Name Link
🔨 Latest commit 9d0e2b8
🔍 Latest deploy log https://app.netlify.com/projects/coasys-we/deploys/6a909b163fa29c0008df778b
😎 Deploy Preview https://deploy-preview-167--coasys-we.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@jhweir
jhweir merged commit 4065143 into dev Aug 27, 2026
9 checks passed
@jhweir jhweir changed the title Feat/workshop space Feature: Workshop space Aug 27, 2026
@jhweir jhweir changed the title Feature: Workshop space Feature: Start on workshop space Aug 27, 2026
@jhweir jhweir changed the title Feature: Start on workshop space Feature: Start on workshop space template Aug 27, 2026
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