Skip to content

feat(greeting): let the first impression be a card, not only a paragraph - #8

Merged
AimTune merged 1 commit into
mainfrom
feat/rich-greeting
Jul 28, 2026
Merged

feat(greeting): let the first impression be a card, not only a paragraph#8
AimTune merged 1 commit into
mainfrom
feat/rich-greeting

Conversation

@AimTune

@AimTune AimTune commented Jul 28, 2026

Copy link
Copy Markdown
Owner

The greeting could carry exactly one thing — a string, mapped to one text frame. Everything the message layer added in 0.3.0 was unreachable from it, so the one message an app is guaranteed to send was its least expressive.

What changes

A greeting now takes a string (unchanged), a described rich message, or a list mixing both — delivered in order, each landing as its own persistent frame:

greeting: (conv) => [
  `Hi ${conv.userId}! What can I do for you?`,
  mekik.messages.buttons.spec({
    buttons: [
      { label: "Track an order", value: "/track" },
      { label: "Start a return", value: "/return" },
    ],
  }),
],
Greeting = conv => new object[]
{
    $"Hi {conv.UserId}! What can I do for you?",
    Messages.ButtonsSpec([Messages.Button("Track an order", "/track"), Messages.Button("Start a return", "/return")]),
},

The one new concept: a described message

Emitting needs a node's ctx, and the greeting fires on connect, outside any run. So this adds the value form of a message — { type, data, id? } — built with mekik.messageSpec / Messages.Spec, or typed per kind: messages.card.spec(data) and Messages.CardSpec(…), the same parameters as the emitter minus the ctx. In .NET the emitters are now one-liners over their own …Spec builders, so each type keeps one parameter list rather than two.

GenUI components are deliberately not accepted there: a chunk belongs to a turn's stream, and on connect there is no stream to attach it to. That's called out where a reader would look for it (messages page, getting-started, PROTOCOL.md §4.5).

Safety and compatibility

  • Reserved frame types are refused by the builders (they throw) and dropped by the engine if a hand-built spec names one — the same rule the mapper applies, so a greeting can't collide with the protocol's own frames.
  • Empty strings still greet nothing.
  • Source-compatible: greeting: () => "hi" still typechecks in both languages (.NET's Func<…, string?> is assignable to Func<…, object?> by covariance).
  • Wire-compatible: no new frame type, no PROTOCOL_VERSION change — the greeting simply emits frames that already existed.

Verification

Unit tests for the spec builders plus engine scenarios in both languages: ordering, the spec's id reaching the wire, gap-free seq across greeting frames, replay-not-re-greet on reconnect, the empty-string skip, and the reserved-type drop. pnpm check green (53 TS tests + every example probe); dotnet test 49/49; Docusaurus build clean.

examples/storefront.ts now greets with prose plus a button message and asserts both frames arrive before any turn.

🤖 Generated with Claude Code

The greeting could carry exactly one thing — a string, mapped to one `text`
frame. Everything the message layer added in 0.3.0 was unreachable from it, so
the one message an app is guaranteed to send was its least expressive.

It now takes a string (unchanged), a **described** rich message, or a list
mixing both, delivered in order. Each item lands as its own persistent frame, so
all of them replay on reconnect and the conversation is still never greeted
twice.

Emitting needs a node's `ctx`, and the greeting fires on connect, outside any
run — so this adds the value form of a message: `{type, data, id?}`. Built with
`mekik.messageSpec` / `Messages.Spec`, or typed per kind:
`messages.card.spec(data)` and `Messages.CardSpec(…)`, the same parameters as the
emitter minus the ctx. The .NET emitters are now one-liners over their own Spec
builders, so each type keeps one parameter list rather than two.

GenUI components are deliberately not accepted there: a chunk belongs to a turn's
stream, and on connect there is no stream to attach it to. The distinction is
documented where a reader would look for it.

Reserved frame types are refused by the builders (they throw) and dropped by the
engine if a hand-built spec names one — the same rule the mapper applies, so a
greeting cannot collide with the protocol's own frames.

Source-compatible: `greeting: () => "hi"` still typechecks in both languages
(.NET's `Func<…, string?>` is assignable to `Func<…, object?>` by covariance).
Covered by unit tests for the spec builders and engine scenarios in both
languages — ordering, ids, gap-free seq, the empty-string skip, and the
reserved-type drop. `examples/storefront.ts` now greets with prose plus a button
message and asserts both frames.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AimTune
AimTune merged commit 8d1ee09 into main Jul 28, 2026
4 checks passed
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