Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
be2e707
Pin the ad4m build whose model layer the rest of this branch takes up
jhweir Sep 7, 2026
94df2a4
Tell a suggestion what it is, and let somebody fix it before keeping it
jhweir Sep 7, 2026
ba6a29e
Ask which call's suggestions are wanted, rather than which were last …
jhweir Sep 7, 2026
23f293b
Ask a card whether anybody agreed to it, and a panel whose decisions …
jhweir Sep 7, 2026
54852a4
Draw a suggestion the way its model asks to be drawn
jhweir Sep 8, 2026
f4cd909
Merge dev into feat/transcript-extraction-graph
jhweir Sep 8, 2026
bf3015e
Let a person write into a transcript, and say which words are theirs
jhweir Sep 8, 2026
4289bcc
Compose into any transcript, and fix crushing in the primitives
jhweir Sep 9, 2026
6a00e58
Stop a space switch being overwritten by the one it replaced
jhweir Sep 9, 2026
58e2721
Actually abbreviate the times in the transcript panel
jhweir Sep 9, 2026
d7450c2
Time a transcript row by the clock once the call is over
jhweir Sep 9, 2026
49c4609
Mark a transcript line with an icon and a word, not two badges
jhweir Sep 9, 2026
d60e94e
Stop we-tooltip and we-menu-group squatting on `title`
jhweir Sep 9, 2026
4532a19
Every tooltip is we-tooltip, and an audit to keep it that way
jhweir Sep 9, 2026
746c400
Fix autoGrow, which never grew, and tidy the mending row
jhweir Sep 9, 2026
3fdecc9
Make one row of textarea one control tall, and unstack the mark from …
jhweir Sep 9, 2026
0851a0e
Narrow the textarea's sides, chip the typed mark, grow the speaker
jhweir Sep 9, 2026
7dccc4f
Say who yields on the transcript row, and four smaller things
jhweir Sep 9, 2026
9323014
Stop we-tooltip existing as a box
jhweir Sep 9, 2026
d0f58a4
Anchor a tooltip to something that has a box
jhweir Sep 9, 2026
b60eff3
Left-pack the transcript row, and stop each line being a card
jhweir Sep 9, 2026
f593ac5
Widen the gap between utterances, and stop the pencil flashing on exit
jhweir Sep 9, 2026
277dab3
Put the composer row at the height every other field stands at
jhweir Sep 9, 2026
ee1e867
Measure a textarea from the outside, like every other control
jhweir Sep 9, 2026
36dd217
Shorten the composer's placeholder
jhweir Sep 9, 2026
0532416
Call a typed line a message everywhere, not three different things
jhweir Sep 9, 2026
2f06073
Hold the layouts section behind the developer switch
jhweir Sep 9, 2026
e5f035c
Take the layouts section out of the picker until it is tested
jhweir Sep 9, 2026
e814f68
Paint a graph's ground with the page role, not a scale step
jhweir Sep 9, 2026
529ca64
Name a conversation in the workshop, and say who was in it
jhweir Sep 9, 2026
0b4b93d
Clear the call pill of the sidebar, and drop the per-row continue
jhweir Sep 9, 2026
7e623dd
Let the rail pick up the call you are looking at
jhweir Sep 9, 2026
6bb6792
Ask whether the call named is live, not whether one is named
jhweir Sep 9, 2026
9c6ff83
Let a selected call be let go of by clicking it again
jhweir Sep 9, 2026
13f6633
Give the workshop's pills the theme's shape, and more presence
jhweir Sep 9, 2026
258ec24
Stop we-text inheriting the browser's heading margins
jhweir Sep 9, 2026
77e38d7
Reserve the band the grown pills actually occupy
jhweir Sep 9, 2026
a423b1e
Say "Transcript" always, and say when there is nothing in one
jhweir Sep 9, 2026
2c541b2
Show the empty state on a past call, and shrink the pill's pencil
jhweir Sep 9, 2026
5430971
Gate the workshop's calendar on a call, and name the call when it is …
jhweir Sep 9, 2026
ff87828
Let a panel explain itself behind a glyph, and hold its header height
jhweir Sep 10, 2026
273e056
Let a launcher say its module is working, and show it in the rail
jhweir Sep 10, 2026
fc52de0
Say on a call's activity that its record was picked back up
jhweir Sep 10, 2026
c7457bf
Rework the transcribe panels around one control per question
jhweir Sep 10, 2026
9977eca
Fix three things the transcribe store got wrong about a call ending
jhweir Sep 10, 2026
4092163
Stop teaching a space token that does not exist, and regenerate
jhweir Sep 10, 2026
f3d5a3e
Keep the level meter up while transcription is switched off
jhweir Sep 10, 2026
b7ac9de
Name the button the panel is pointing at
jhweir Sep 10, 2026
bca9854
Give the extraction panel one thing to say outside a call
jhweir Sep 10, 2026
f81610d
Take the extraction controls off the chips' ground, and name the chips
jhweir Sep 10, 2026
6d54103
Keep Extract now at one weight
jhweir Sep 10, 2026
8fcb40e
Give a module a way to say something outside its own panels
jhweir Sep 10, 2026
a54a5d3
Point at where models come from, and stop narrating the ones that are…
jhweir Sep 10, 2026
cbc3a58
Give the actual reason Extract now cannot be pressed
jhweir Sep 10, 2026
8de8226
Disable Extract now until somebody has actually spoken
jhweir Sep 10, 2026
fc8a58f
Give the extraction panel's regions one heading treatment
jhweir Sep 10, 2026
275afba
Give a suggestion card a headline, a corner and a bottom line
jhweir Sep 10, 2026
6f59756
Stop restating what a pass produced above the chips
jhweir Sep 10, 2026
6a66e0a
Quieten a suggestion's details, and edit its dates with a calendar
jhweir Sep 10, 2026
691b87b
Offer a community's own task states wherever a status is picked
jhweir Sep 10, 2026
88d3067
Tell a stored pass what started it, and what it asked
jhweir Sep 10, 2026
36af877
Write down a watched pass, the way a pressed one already was
jhweir Sep 10, 2026
a7fe776
Open a reading of a call onto what the model was asked
jhweir Sep 10, 2026
149760d
Let an empty state say its sentence either way
jhweir Sep 10, 2026
5449046
Regenerate the AI context
jhweir Sep 10, 2026
398c2a9
Put extraction's verb in its header, and name the log
jhweir Sep 10, 2026
f83c54b
Swap extraction's two controls between header and well
jhweir Sep 10, 2026
9621d56
Stop a call card calling a transcriber method that is gone
jhweir Sep 10, 2026
7a088cd
Move the way back into a call onto the call's own name
jhweir Sep 10, 2026
38c2841
Give the pick-up button the size of the thing beside it
jhweir Sep 10, 2026
2b0791c
Let the pill's two buttons size their own icons, and rebalance it
jhweir Sep 10, 2026
8b48c4b
Hide the extraction panel's two readouts until they have something
jhweir Sep 10, 2026
c3b261d
Keep the pill's call button on screen, and turn it red in the call
jhweir Sep 10, 2026
94f4943
Stop listing every reading twice in the extraction panel
jhweir Sep 10, 2026
57f1b0f
Make a stored exchange readable, and say "things found"
jhweir Sep 10, 2026
9d7be67
Mark the awaiting section once, not once per card
jhweir Sep 10, 2026
45f1e50
Draw extracted records as the suggestions above them are drawn
jhweir Sep 10, 2026
4496689
Show only the fields an extracted record has something in
jhweir Sep 10, 2026
89d4465
Record which records a pass wrote, as a relation on the collection
jhweir Sep 10, 2026
6cb65cc
Read a call's results in one query, sorted, and count them
jhweir Sep 10, 2026
b1f2937
Start a call rather than reopening the one selected in the list
jhweir Sep 10, 2026
801c845
Give calls a corner that is always there
jhweir Sep 10, 2026
3f617c7
Call startCall with an argument, and sit it in the band
jhweir Sep 10, 2026
b5e1e17
Stop the transcript claiming nothing was said while it is being said
jhweir Sep 10, 2026
6936577
Let the call corner say one thing at a time
jhweir Sep 10, 2026
23a3ce3
Scroll the extraction panel as one thing, and fold its lists
jhweir Sep 10, 2026
9edfd65
Merge remote-tracking branch 'origin/dev' into feat/transcript-extrac…
jhweir Sep 11, 2026
da62953
Typecheck the two casts and the build flag @we/primitives cannot make
jhweir Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions .cursor/rules/we-schema.mdc

Large diffs are not rendered by default.

29 changes: 18 additions & 11 deletions .github/copilot-instructions.md

Large diffs are not rendered by default.

29 changes: 18 additions & 11 deletions AGENTS.md

Large diffs are not rendered by default.

29 changes: 18 additions & 11 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions docs/architecture/chrome-and-panels.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ Four rules, each of which was broken by at least one panel before the fragment e
- **One padding.** `300`, which `panelShell` applies. Panels share edges, and two paddings read as a
wobble.

**A panel explains itself behind a glyph, not in its body.** `help` on either fragment puts an info
glyph against the name; pointing at it (or focusing it) shows two to four sentences on how the panel
works. The extraction panel used to say all of that in its body — a lead-in above the chips, a
footnote under the button — and it was read once and then furniture, in a docked panel with no room
to spare. Prose about _how something works_ goes in `help`; prose about _the current state_ (why a
control is disabled, what a pass found) stays in the body, because it is news rather than a manual.
Not every panel wants one: a glyph on every header is noise of its own.

```ts
panelShell({ title: 'Extraction', help: 'A model reads the transcript and writes what it finds …', children });
```

TSX panels cannot call a schema fragment, so they spread `PANEL_TITLE_PROPS` onto a `we-text` —
one definition, two languages.

Expand Down
92 changes: 74 additions & 18 deletions packages/ai-context/context.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/ai-context/src/fragments/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const rules = `
- For icon-only buttons, nest a \`we-icon\` child inside \`we-button\` rather than using a \`text\` prop with a Unicode character. **Omit the \`size\` prop on \`we-icon\` when nesting inside sized primitives** (\`we-button\`, \`we-input\`, \`we-badge\`, \`we-textarea\`) — these components auto-size nested icons via \`--we-context-icon-size\` (xs→12px, sm→16px, md→24px, lg→32px, xl→40px). Only set an explicit icon \`size\` if you need to override the automatic sizing. Example: \`{ type: 'we-button', props: { variant: 'ghost', size: 'sm' }, children: [{ type: 'we-icon', props: { name: 'x' } }] }\`.
- NEVER pass a bare number like "16" as a size or dimension prop — it is not valid CSS. Always check the component's declared prop type: if it's a string union, use one of the listed values; if it accepts arbitrary strings, include a CSS unit (e.g. "16px", "2rem").
- For interactive list items and selectable options, use \`we-button\` with variant switching (e.g., \`secondary\` when selected, \`ghost\` when not) instead of manually styling \`Row\` with cursor, bg, and onClick. Buttons provide hover, focus, and active states for free.
- To make a block of content clickable **without any button appearance**, use \`we-button\` with \`variant: 'bare'\` — never a \`Column\`/\`Row\` with an \`onClick\`. \`bare\` is the appearance-free variant: no background, no hover, no padding, no radius, inherited colour — but still a real \`<button>\`, so it keeps keyboard activation, the \`disabled\` prop, and the accessibility role that a clickable \`Column\` silently loses. Do not use \`ghost\` for this: ghost's hover background is deliberate, and it paints a rectangle over content that supplies its own affordance. Example: \`{ type: 'we-button', props: { variant: 'bare', disabled: { $: 'someStore.busy' }, onClick }, children: [{ type: 'Column', props: { gap: '150', ax: 'center' }, children: [...] }] }\`.
- To make a block of content clickable **without any button appearance**, use \`we-button\` with \`variant: 'bare'\` — never a \`Column\`/\`Row\` with an \`onClick\`. \`bare\` is the appearance-free variant: no background, no hover, no padding, no radius, inherited colour — but still a real \`<button>\`, so it keeps keyboard activation, the \`disabled\` prop, and the accessibility role that a clickable \`Column\` silently loses. Do not use \`ghost\` for this: ghost's hover background is deliberate, and it paints a rectangle over content that supplies its own affordance. Example: \`{ type: 'we-button', props: { variant: 'bare', disabled: { $: 'someStore.busy' }, onClick }, children: [{ type: 'Column', props: { gap: '100', ax: 'center' }, children: [...] }] }\`.
- For card-like layouts, compose from \`Column\` with DS props (bg, r, border, p, gap). This gives full control over spacing and appearance.
- When rendering lists of similar items (posts, cards, users, etc.), ALWAYS use \`$each\` with a single template child — never duplicate the same node structure multiple times. Use literal arrays in \`items\` for static data, or a store read/\`$query\` for dynamic data.

Expand Down
6 changes: 3 additions & 3 deletions packages/ai-context/src/fragments/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ export const storeEntries: StoreEntry[] = [
},
moduleLaunchers: {
type: 'array',
properties: ['id', 'icon', 'label', 'active'],
properties: ['id', 'icon', 'label', 'active', 'busy'],
},
taskStates: { type: 'array', properties: ['id', 'name', 'slug', 'semantic', 'color', 'retired', 'defined'] },
offeredTaskStates: {
Expand Down Expand Up @@ -1014,7 +1014,7 @@ export function generateStoresText(entries: StoreEntry[]): string {
moduleInstallSettings:
"{ id, name, description, icon, installed, surface, switchable }[] — every registered module and whether this agent wants it anywhere. The global Settings → Modules list, and the only place an 'app' or 'capability' module is decided about: a contribution is gated at the layer where it renders, and only 'chrome' renders inside a space. `surface` is derived from what the module contributes. Its per-space counterpart is `modules` on each spaceList row, which carries enabled/installed/visible/active together and lists chrome modules only",
moduleLaunchers:
'{ id, icon, label, active }[] — launchers for the modules enabled here and available in this space; what the host module rail renders. Pair with { $action: "spaceStore.launchModule", args: [{ $: "mod.id" }] }',
'{ id, icon, label, active, busy }[] — launchers for the modules enabled here and available in this space; what the host module rail renders. `active` lights the button while the module reports its surface open; `busy` says the module is working in the background — an extraction pass running — and is independent of `active`, so a rail can show work going on behind a closed panel. Pair with { $action: "spaceStore.launchModule", args: [{ $: "mod.id" }] }',
},
actions: {
moveChild:
Expand Down Expand Up @@ -1144,7 +1144,7 @@ export function generateStoresText(entries: StoreEntry[]): string {
creatableEntities:
"{ label, value, icon, group }[] — models a person can create an instance of here, ready for a we-select: this space's own models first, then WE's built-in content types. A model appears here by declaring `authoring` in the manifest, or by being a shape this community defined",
displays:
"Record<entity, RecordDisplay> — how to show an instance of each creatable model, keyed by entity name and derived from its declaration: { entity, label, icon, title, summary, media, fields[] }, where title/summary/media name the properties playing those roles ('' when none does) and each field is { name, label, kind, role }. kind is one of text, longText, number, boolean, date, datetime, color, url, image, file, json; role is title, summary, media or detail. Index it by a row's type — { $: 'recordStore.displays[row.type]' } — and render the fields with $each; see \"A record of any type\" in the patterns",
"Record<entity, RecordDisplay> — how to show an instance of each creatable model, keyed by entity name and derived from its declaration: { entity, label, icon, title, summary, media, fields[] }, where title/summary/media name the properties playing those roles ('' when none does) and each field is { name, label, kind, role, options }. kind is one of text, longText, number, boolean, date, datetime, color, url, image, file, json; role is title, summary, media or detail. `options` is the values a field is allowed to hold where the model closes the set (a task's status), empty otherwise — count() it to tell a state worth drawing as a we-badge from free text, and map it into a we-select rather than offering a text box that accepts a word the model does not know. Index it by a row's type — { $: 'recordStore.displays[row.type]' } — and render the fields with $each; see \"A record of any type\" in the patterns",
recordDraft:
"the open form's draft ({ entity, label, icon, fields[] }) or null while closed — its non-nullness is what mounts the modal. Each field is { name, label, control, required, options, placeholder, value }, derived from the model's own declaration, so a form exists for a model nobody wrote a form for",
recordDraftDirty:
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-context/src/schemaContext.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function Section(props: { panel: TemplatePanel }) {
not exist on a touchscreen, and a grip somebody cannot tab to is a control some people
cannot use. The tooltip carries the verb.
*/}
<we-tooltip title="Break out into a panel" placement="left">
<we-tooltip content="Break out into a panel" placement="left">
<we-move-handle
label={`Break ${props.panel.title ?? props.panel.id} out into a panel`}
style={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { datasetAddressedBy } from '@shared/datasetIdentity';
import { queryIRFlag } from '@shared/queryIRFlag';
import { provideModuleHostServices } from '@shared/registries/moduleHostServices';
import { resolveParts, resolvePartsInRoutes } from '@shared/registries/moduleParts';
Expand Down Expand Up @@ -149,6 +150,11 @@ export default function TemplateProvider() {
return getEntity(entity).create(perspective, fields, Object.keys(rest).length ? rest : undefined);
}

/** The same, for `record.update` — `recordActions` resolves a store *path*, and a module has a handle. */
function updateInDataset(entity: string, id: string, fields: Record<string, unknown>, perspective: DatasetProxy) {
return getEntity(entity).update(perspective, id, fields);
}

// The same capability schemas get as `record.create`, lent to module stores that must write
// without a click to hang a schema action on — a transcript appears because somebody spoke.
onCleanup(
Expand Down Expand Up @@ -227,6 +233,16 @@ export default function TemplateProvider() {
}
await (add as (v: string) => Promise<void>).call(instance, value);
},

// The scalar counterpart of `linkEntity`, resolved the same way `createEntity` is: a module
// names a dataset by URI, and an unresolvable name refuses rather than writing to whatever is
// on screen. See `ModuleStoreDeps.updateEntity` for why a module needs this when a schema's
// `record.update` already exists.
updateEntity: async (entity, id, fields, options) => {
const p = moduleTarget(options?.dataset);
if (!p) return;
await updateInDataset(entity, id, fields, p);
},
}),
);

Expand Down Expand Up @@ -729,6 +745,28 @@ export default function TemplateProvider() {
const segments = routeStore.segments();
if (segments[0] !== 'space' || !segments[1]) return;

/*
Only while the address on screen is about the space these sections belong to.

Everything below reads the *store's* space — its nav, its enabled sections — and writes the
*URL's* space. Those are the same space almost always, and the window where they are not is
the one that mattered: `navigateToSpace` switches the dataset first and navigates second, so
between the two the stores describe B while the URL still says A. This effect woke in that
window, because the template had just been replaced with B's default and the previous one was
self-routing — so `hasViewsMarker` went from false to true, on a URL holding a self-routing
template's own path, which is a section no space has.

It then did exactly what it is written to do, to the wrong space: rewrote the URL to
`/space/A/<B's first section>`. That is a route change naming A, so the route effect that
keeps the dataset in step with the address dutifully switched *back* to A — landing after the
switch to B had finished, since it starts later. The reader ended up in space B by every sign
the URL and the sidebar could give, reading space A's records.

The guard is the invariant stated plainly: correct an address only when it is an address about
the space you are reading from.
*/
if (!datasetAddressedBy(datasetStore.currentDataset(), segments[1])) return;

/*
Only for a template whose sections these are.

Expand Down
Loading
Loading