@astrosheep/square 0.3.88 → 0.3.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/artifact.js +11 -9
  2. package/dist/boundary-presentation.d.ts +3 -2
  3. package/dist/boundary-presentation.js +50 -77
  4. package/dist/delivery-deferral.d.ts +28 -0
  5. package/dist/delivery-deferral.js +83 -0
  6. package/dist/delivery-operations.js +8 -6
  7. package/dist/delivery.js +4 -1
  8. package/dist/inbox.js +21 -10
  9. package/dist/model.d.ts +1 -0
  10. package/dist/ports.d.ts +11 -3
  11. package/dist/presentation-operations.d.ts +20 -1
  12. package/dist/presentation-operations.js +68 -44
  13. package/dist/sqlite-square.js +29 -0
  14. package/dist/square-domain.d.ts +1 -1
  15. package/dist/square-domain.js +12 -0
  16. package/dist/square-facade.d.ts +8 -0
  17. package/dist/square-migration.d.ts +2 -1
  18. package/dist/square-migration.js +25 -26
  19. package/dist/square-sqlite.d.ts +5 -2
  20. package/dist/square-sqlite.js +79 -6
  21. package/dist/square-stdio.d.ts +1 -1
  22. package/dist/square-stdio.js +30 -17
  23. package/dist/stdio-server.js +23 -3
  24. package/guides/backends.md +26 -13
  25. package/guides/change-waits.md +40 -11
  26. package/guides/migration.md +30 -12
  27. package/guides/operation-cancellation.md +16 -1
  28. package/guides/participant-ownership.md +60 -186
  29. package/guides/square-api.md +72 -9
  30. package/package.json +1 -1
  31. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  32. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  33. package/plugins/paseo/README.md +7 -6
  34. package/plugins/paseo/client/markdown.tsx +125 -185
  35. package/plugins/paseo/client/square-message.tsx +9 -25
  36. package/plugins/paseo/client/transform.ts +5 -4
  37. package/plugins/paseo/index.client.tsx +14 -12
  38. package/plugins/paseo/package.json +3 -0
  39. package/plugins/paseo/shared/message.ts +50 -1
@@ -1,186 +1,60 @@
1
- # Participant Ownership
2
-
3
- This document defines the ownership boundary for a participant name, a host
4
- session, and its wake route. It is the contract for ordinary `join`, automatic
5
- session startup, explicit `--kick`, and session shutdown.
6
-
7
- The central invariant is simple:
8
-
9
- > At most one live host session owns a participant name in one square.
10
-
11
- An artifact participant and a host session are related, but they are not the
12
- same fact. The artifact is authoritative for the participant lifecycle. The
13
- host ledger is authoritative for the local session binding. A wake route is
14
- only a callable address for an existing binding.
15
-
16
- The uniqueness key is the canonical square location plus `nameKey(name)` (the
17
- same normalized identity used by the artifact). Display spelling does not
18
- create a second participant.
19
-
20
- ## Owners
21
-
22
- | Fact | Authority | May change it |
23
- | --- | --- | --- |
24
- | Participant name is known, active, or done | Square artifact (`SquareState`) | Square actions in an artifact transaction |
25
- | Session owns a name at a location | Host ledger presence | Registry/ledger binding operations |
26
- | Session can be woken | Wake route in the square artifact | Route operations after the lifecycle commit |
27
- | CLI or hook requested join, kick, or shutdown | CLI or host adapter input | The caller only; it does not become a new authority |
28
-
29
- No layer may use display text, a route row, or an old registry row as a
30
- replacement for the authoritative fact it does not own.
31
-
32
- ## Call Boundaries
33
-
34
- The artifact boundary and the host boundary exchange facts, not each other's
35
- storage objects:
36
-
37
- - `square-core` and participant lifecycle actions decide and commit activities
38
- (`join`, `done`, takeover) and artifact-owned wake routes. Their post-commit
39
- presence refresh is best effort; they do not discover foreign sessions,
40
- claim names, or remove another session's presence.
41
- - The host ledger claims and releases `(location, name, session, channel)`
42
- bindings. It does not append participant activities and it does not decide
43
- whether a name is active in the artifact.
44
- - The name claim must be an atomic compare-and-set on `(location, name)`:
45
- observe-then-upsert (`listPresence` followed by `ensurePresence`) is not a
46
- claim and cannot enforce the one-owner invariant under concurrency.
47
- - CLI and hook orchestration obtains the current session identity, sequences
48
- the two boundaries, and performs exact-session cleanup. It may pass a
49
- participant name and a lifecycle intent to an artifact operation; it must
50
- not pass host binding rows or `oldSessionIds` into a square action.
51
- - Route publication and retirement write the artifact route set. A route is
52
- useful only while its exact session binding is owned; route rows are not a
53
- second name-ownership ledger.
54
-
55
- The intended shape is therefore `claim/verify -> artifact transaction ->
56
- presence/route effects`. A helper that combines those steps is an orchestration
57
- helper, not a square action. In particular, a `takeover(name,
58
- oldSessionIds)`-style API is the wrong boundary: the artifact can retire the
59
- active participant routes by name, while the host adapter cleans old presence
60
- by the exact sessions it observed.
61
-
62
- ## Ordinary Join
63
-
64
- `join` is a name claim, not a reconnect heuristic.
65
-
66
- 1. Validate the requested participant name.
67
- 2. Ask the host ledger to claim the `(location, name)` binding for the current
68
- session. An existing binding for another session is a hard refusal. A
69
- binding that this same session already held is an idempotent reconnect. A
70
- newly-created claim is provisional until the artifact check below passes.
71
- 3. In one artifact transaction, decide and commit the participant `join`
72
- activity when the name is not active. If the artifact already says that the
73
- name is active, only a pre-existing binding for this exact session may
74
- reconnect; release a provisional claim and refuse every other caller.
75
- 4. After the artifact commit, ensure presence for the current session only.
76
- 5. After presence is established, publish the current session's primary wake
77
- route.
78
- 6. Render the entry result from the committed artifact snapshot. Rendering
79
- does not perform another ownership decision.
80
-
81
- If any precondition fails, no join activity, presence row, or route is created
82
- left behind for the refused session. A reconnect does not append another `join`
83
- activity.
84
-
85
- ## Automatic Session Startup
86
-
87
- Automatic startup is a host adapter around the same ownership semantics. It is
88
- not an implicit kick and it does not get a separate exception for configured
89
- names.
90
-
91
- 1. Derive the participant name from the provider session and environment.
92
- 2. Attempt the host-ledger binding claim.
93
- 3. If another session owns the name, return the coded `already_joined`
94
- refusal. Do not rewrite the artifact, remove the other session, or create a
95
- second binding.
96
- 4. If the current session already owns the binding, refresh its presence and
97
- route without another artifact `join`.
98
- 5. If the name is new, commit the single artifact `join`, then establish
99
- presence and route as in ordinary join.
100
-
101
- The hook boundary catches and reports this refusal according to host policy;
102
- the hook must not silently turn it into a takeover.
103
-
104
- ## Explicit Kick
105
-
106
- `join --kick` is the only name-takeover operation. It is an explicit human
107
- action and must leave an observable lifecycle, not merely overwrite a ledger
108
- row.
109
-
110
- 1. Resolve the requested name and collect the currently active bindings for
111
- that name. The current session is excluded from the old-owner set only when
112
- it is already the owner and this is a reconnect, not a kick.
113
- 2. In one artifact transaction, commit two lifecycle activities:
114
- `done` for the standing participant, followed immediately by `join` for the
115
- reclaimed name. If this transaction cannot commit, nothing is kicked.
116
- 3. In the same artifact transaction, remove every route for the kicked
117
- participant. The artifact operation receives only the participant name; it
118
- does not receive host session rows. The new route is not published until
119
- after the transaction succeeds.
120
- 4. After the artifact commit, remove every old owner's presence from both
121
- readable ledger scopes. The cleanup is keyed by the exact old session and
122
- channel; never remove by participant name alone.
123
- 5. Establish the new session's presence and publish its route.
124
- 6. Reconcile once from the committed artifact and ledger facts. Reconciliation
125
- may remove stale external rows, but it may not invent a join or undo the
126
- committed takeover.
127
-
128
- The resulting history must contain `join -> done -> join`. A successful kick
129
- must leave no old binding and no old callable route. If external cleanup is
130
- temporarily unavailable, the artifact still records the kick and the cleanup
131
- remains retryable and session-scoped.
132
-
133
- ## Session Shutdown
134
-
135
- Session shutdown is owner-scoped.
136
-
137
- 1. Look up the binding by the exact session id and square location in the host
138
- ledger.
139
- 2. If no binding exists, stop without changing the artifact.
140
- 3. If the binding exists and its participant is still active, ask the artifact
141
- boundary to commit that participant's `done` activity. The artifact action
142
- does not need the session id.
143
- 4. Remove that exact session's presence in the host ledger and retire that
144
- exact session's route in the artifact.
145
- 5. Do not mark a replacement session done, even when it uses the same display
146
- name. Under the invariant, such a replacement should have been rejected or
147
- created through `--kick`.
148
-
149
- A shutdown arriving after a kick is an idempotent cleanup of an already-removed
150
- owner. It must not append a second `done` for the new owner.
151
-
152
- ## Concurrency and Recovery
153
-
154
- The artifact transaction serializes participant lifecycle activities. The host
155
- ledger lock serializes binding rows. They protect different facts and must not
156
- be replaced with a copied sidecar or a display-level mutex.
157
-
158
- The safe ordering is:
159
-
160
- 1. Claim or verify the session binding.
161
- 2. Check the artifact state and commit its lifecycle transaction.
162
- 3. Apply presence and route effects for the committed result.
163
- 4. Retry or reconcile only the external effects that did not commit.
164
-
165
- An operation must never claim success from a partially completed sequence. A
166
- failed precondition produces no artifact mutation. A post-commit ledger or
167
- route failure produces a diagnosed, retryable external inconsistency while
168
- preserving the artifact's committed lifecycle.
169
-
170
- ## Acceptance Evidence
171
-
172
- The implementation is complete only when these invariants are directly tested:
173
-
174
- - two sessions cannot concurrently own the same `(location, name)`;
175
- - same-session reconnect is idempotent and appends no second `join`;
176
- - automatic startup refuses a foreign owner and never performs a kick;
177
- - `join --kick` records `done -> join` atomically;
178
- - kick cleanup removes old presence and routes by exact session identity;
179
- - a kicked session's later shutdown cannot close the replacement;
180
- - a failed external cleanup is retryable without duplicating lifecycle
181
- activities;
182
- - reconciliation removes stale external rows only when the artifact proves
183
- the participant is no longer active.
184
-
185
- Tests should assert the authoritative facts and their ownership boundaries,
186
- not the order of helper calls or the wording of CLI output.
1
+ # Participant ownership
2
+
3
+ A participant has one current artifact-owned seat: its normalized participant name, exact Body
4
+ (`channel`, `session`, optional `agent`), establishing join generation, and optional wake address.
5
+ Display spelling and discovery-index entries never grant write authority. The artifact transaction
6
+ is the sole custody authority; there is no writable HostLedger ownership layer.
7
+
8
+ ## Captured handles and lifecycle
9
+
10
+ `Square.at` captures an explicit Body for writes. `participant(name?)` selects a handle without
11
+ joining; `recognize()` returns current handles held by that exact Body. A name alone is not
12
+ permission to mutate. Read-only archive operations need no Body. See [square-api.md](square-api.md)
13
+ for the public resource lifetime and selection contract.
14
+
15
+ - `join(name)` atomically lands the join and stores its seat. Same-body reconnect preserves its
16
+ generation and may refresh the wake address. A foreign Body receives `already_joined` rather
17
+ than taking ownership implicitly.
18
+ - `join(name, {takeover:true})` explicitly completes the captured old seat and establishes a new
19
+ one with `done -> join` in one transaction. Its compare-and-set generation prevents a concurrent
20
+ takeover from being silently replaced.
21
+ - Participant mutations carry the captured seat into the transaction. The gate checks both Body
22
+ and generation, including when the same Body later reacquires the name. An old handle never
23
+ becomes fresh because a name matches again.
24
+ - `done` retires only the current captured seat, catch lease and delivery deferrals. A later join
25
+ establishes a fresh generation. Completed coordinates retained for exact-body diagnostics are
26
+ not current write authority. Owner-session cleanup uses the same fence and cannot complete a
27
+ replacement seat.
28
+ - Stdio retains the selected Participant in a connection-local reference. Each operation uses that
29
+ captured handle instead of reselecting authority by name. Rejoin/takeover therefore fences old
30
+ remote handles just as it does local SDK handles.
31
+
32
+ The HOME discovery index is rebuildable candidate-path data. Losing it can delay discovery but
33
+ cannot transfer a seat or erase delivery evidence. A wake address is only transport readiness for
34
+ an existing seat, not an alternative ownership fact.
35
+
36
+ ## Independent delivery deferrals
37
+
38
+ `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral` require that same captured
39
+ current seat. Deferrals live in the artifact, keyed by normalized participant, generation and fresh
40
+ UUID, independently of the single catch lease and of delivery claims/receipts. A lease contains
41
+ only its validated metadata match and expiry; it is not an acknowledgement intent or an activity.
42
+
43
+ Each lease's nested own-property scalar equalities combine with AND; matching active leases combine
44
+ with OR. Renewal cannot revive an expired lease. Release touches only its named live lease, and
45
+ returns false for an absent/expired ID. A current owner cannot use another participant's ID.
46
+ Expired rows may be pruned during owner writes; read/claim eligibility always checks expiry and
47
+ the current generation. Done/takeover/rejoin remove deferrals in the same transaction, and stale
48
+ rows can never become active for a new generation.
49
+
50
+ Deferrals change only proactive eligibility. They preserve audience, history, unread counts and
51
+ explicit catch. An explicit catch consumes normally and prevents later replay after release.
52
+ All proactive paths recheck the same typed policy at claim and final send/presentation boundaries.
53
+ An already-dispatched external message cannot be recalled by acquiring a lease.
54
+
55
+ ## Acceptance boundaries
56
+
57
+ Tests exercise foreign and stale handles, same-body reconnection/rejoin, explicit takeover,
58
+ transaction-scoped generation fences, independent leases, precise expiry, explicit catch and
59
+ stdio parity. Assertions belong to the authoritative facts, not helper call ordering or display
60
+ copy. Migration of old owner/route facts is offline and explicit; see [migration.md](migration.md).
@@ -71,7 +71,8 @@ replacement is visible even while the resource holds its original connection. It
71
71
  its observation lifetime: `return()` interrupts a pending pull; closing the Square does not close
72
72
  that independent path observation. Internal readers that explicitly borrow an artifact port leave
73
73
  its lifetime with the lender and propagate port failures rather than waiting for path repair.
74
- Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `hold`, `resume`, `done`;
74
+ Participant owns `express`, `catch`, `listen`, `unlisten`, `ignore`, `listening`, `hold`, `resume`, `done`,
75
+ `deferDelivery`, `renewDeliveryDeferral`, and `releaseDeliveryDeferral`;
75
76
  its `history` delegates to the same Square history operation.
76
77
 
77
78
  - Express waits while held/throttled unless `noWait: true`, for **every** caller. `ExpressResult`
@@ -86,10 +87,11 @@ its `history` delegates to the same Square history operation.
86
87
  catch, status/latest, unread projections and stream records through the SDK, stdio, CLI and stream.
87
88
  MCP and Pi tools are not a metadata surface: their `express` requires a body and its schema rejects a
88
89
  `metadata` argument, and every shared tool receipt omits metadata, so tool history/catch results carry
89
- the same activities without it. Metadata does not affect mentions, replies, listening, audience, delivery,
90
- ordering, consumption or permissions. Metadata may nest up to 256 levels; the bound is enforced before
90
+ the same activities without it. Metadata does not affect mentions, replies, listening, audience,
91
+ ordering, consumption or permissions. A current participant may temporarily defer proactive delivery
92
+ by exact metadata match through the SDK/stdio lease API below. Metadata may nest up to 256 levels; the bound is enforced before
91
93
  persistence and public reads. Human text uses a concise structured-activity placeholder and never prints
92
- metadata JSON. There is no metadata filter, query language or index; CLI `--metadata <json>` is the only
94
+ metadata JSON. There is no archive metadata filter, query language or index; CLI `--metadata <json>` is the only
93
95
  metadata input flag.
94
96
  - Catch returns `{activities, consumedThrough, remaining, idleExpired, status?, observation?}`.
95
97
  Omitted/zero idle is immediate; positive idle uses the single leased quiet wait. Receipt selection,
@@ -112,6 +114,64 @@ its `history` delegates to the same Square history operation.
112
114
  Every operation accepts its final `OperationControl` (`signal`, optional typed `onProgress`).
113
115
  Listener/lifecycle operations take optional observation options before that control argument.
114
116
 
117
+ ## Temporary proactive delivery deferral
118
+
119
+ SDK and stdio Participants expose the same three operations. There are no CLI/MCP/Pi authoring
120
+ commands or listing operation in this delivery. Existing proactive adapters still obey deferrals.
121
+
122
+ ```ts
123
+ const lease = await alice.deferDelivery({
124
+ match: { fields: [
125
+ { path: ['job', 'kind'], equals: 'outcome' },
126
+ { path: ['job', 'id'], equals: 7 },
127
+ ] },
128
+ durationMs: 60_000,
129
+ });
130
+ try {
131
+ // Explicit history and catch remain available, including catch({id}).
132
+ const renewed = await alice.renewDeliveryDeferral(lease.id, 60_000);
133
+ // null means absent, expired, or not owned by this current generation.
134
+ } finally {
135
+ await alice.releaseDeliveryDeferral(lease.id);
136
+ }
137
+ ```
138
+
139
+ - `MetadataScalar` is `null | boolean | number | string`; numbers must be finite. `MetadataMatch`
140
+ has nonempty `fields`, each `{path: readonly string[], equals: MetadataScalar}`. Paths have one
141
+ or more nonempty string segments, with no duplicates. Only JSON object own properties are
142
+ traversed: arrays, missing/nonobject intermediates and inherited properties do not match.
143
+ Equalities are exact and type-sensitive. A dot inside one segment is a literal key, not path
144
+ syntax. All fields combine with AND; any matching live lease blocks delivery (OR across leases).
145
+ - Each acquire returns a fresh UUID and an operation-clock ISO `expiresAt`: `{id, expiresAt}`.
146
+ Match objects are not returned. `durationMs` must be a positive safe integer and is capped at
147
+ 24 hours. Invalid matches, durations and IDs refuse with `invalid_args` before mutation.
148
+ - Renewal extends that same live lease from the renewal operation's clock; expired/missing IDs
149
+ return `null`, and release returns `false`. Independent overlapping leases are never merged;
150
+ releasing one cannot end another. A captured current Body/join generation is required for every
151
+ mutation. Stale handles retain the ownership refusal; done/takeover/rejoin retire old leases.
152
+ - Lease writes change delivery revision, not conversation history. They do not create a receipt,
153
+ change audience or unread counts, hide an archive record, or prohibit explicit catch. Catch alone
154
+ consumes normally; a caught record is not replayed when a deferral ends. The catch lease is a
155
+ separate, single quiet-wait lease with its existing filters and lifecycle.
156
+ - Producer/sweep, atomic claims, session/native projections, final pre-send checks and proactive
157
+ presentation all share current-generation unexpired matching. Suppressed items get no seen or
158
+ presented receipt. An active pending observer rechecks on release and the earliest relevant
159
+ expiry, including renewed deadlines, without needing a new conversation activity. A one-shot
160
+ producer skips and returns promptly. With no active observer, eligibility is reconsidered at
161
+ the next normal opportunity; no daemon or per-message timer is created.
162
+ - Acquiring a lease is not recall. An external send already past its final freshness check can race
163
+ with acquisition and remain dispatched; a sink effect already written cannot be withdrawn.
164
+ Presentation eligibility is checked before the effect, not again as a veto after success. A batch
165
+ claims and filters its items before its one sink call, then records exactly the successfully rendered
166
+ set under each captured Body/join generation. A lease acquired while the sink returns or before a
167
+ later item's evidence commit does not erase success or allow replay after release. Cancellation
168
+ likewise stops pre-effect work, not truthful completion after a successful sink: evidence settles
169
+ on the original connection with an independent bounded storage lifetime and the same seat fences.
170
+ A rejected/cancelled native landing is not a successful sink. Items filtered
171
+ out before the sink receive no success receipt.
172
+ Cancellation before commit prevents the lease mutation; after commit it remains until explicit
173
+ release, expiry, or seat retirement. See [operation-cancellation.md](operation-cancellation.md).
174
+
115
175
  ## Optional observations, not another execution path
116
176
 
117
177
  Request `observe: true` on the **same method** when rendering needs additional facts. Each operation
@@ -145,13 +205,16 @@ opening a valid current-format Square first.
145
205
 
146
206
  ## Artifact format and explicit migration
147
207
 
148
- The current artifact format is 4. It owns participant seats, delivery evidence and cancellation,
149
- separate from conversation activities; `unlisten` remains a real history activity. Ordinary opens
208
+ The current artifact format is 5. It owns participant seats, delivery evidence, cancellation and
209
+ independent delivery deferrals, separate from conversation activities and catch leases; `unlisten` remains a real history activity. Ordinary opens
150
210
  refuse older formats with migration guidance and never convert them implicitly.
151
211
 
152
- Stop all sessions and services using the artifact and freeze its legacy HostLedger, back them up
153
- offline, then run `square --location <path.square> migrate` and `doctor` before reopening with upgraded
154
- runtimes. Supported row formats 2/3 convert transactionally only when owner/route/join mappings are
212
+ Stop all sessions and services using the artifact, make an offline backup, then run
213
+ `square --location <path.square> migrate` and `doctor` before reopening with upgraded runtimes.
214
+ For formats 2/3, also freeze and back up the legacy HostLedger used for owner/evidence import.
215
+ Format 4 adds only an empty deferral table and its owner/generation/expiry index, preserving
216
+ all existing facts and revisions without consulting HostLedger. Supported row formats 2/3 convert
217
+ directly into format 5 in one transaction, only when owner/route/join mappings are
155
218
  directly verifiable; no seat is manufactured for done participants. Replay-critical native ambiguity
156
219
  refuses rather than disappears, including the conservative refusal of any nonempty legacy token-only
157
220
  tombstone table. Current-format migration is a byte-preserving no-op. See [migration.md](migration.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.88",
3
+ "version": "0.3.90",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.88",
3
+ "version": "0.3.90",
4
4
  "description": "Native Claude Code inbox delivery for Square participants",
5
5
  "types": "./types.d.ts",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.88",
3
+ "version": "0.3.90",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -29,16 +29,17 @@ The collapsed heading uses `metadata.details.items`; it never reverse-parses
29
29
  the content. Expansion shows `metadata.details.fullText` when available: complete
30
30
  notification bodies, even when the model preview was clipped or omitted. Older
31
31
  messages fall back to `detail.text`, including clipping notices and catch commands.
32
+ Each displayed body is capped at 32,000 UTF-16 units (omission line included), keeping
33
+ the beginning and end; collapsed previews are capped at 1,000 units before layout.
34
+ Surrogate pairs are not split. Original messages and model input remain unchanged.
32
35
  Long content scrolls inside a bounded panel. The
33
36
  card does not fetch extra activity or mark anything seen. Live events and fetched history use
34
37
  the same transformer.
35
38
 
36
- The card is visually marked as Square: an accent glyph, a SQUARE badge, and the
37
- square's name sit above the actor line, on a raised surface distinct from ordinary
38
- tool output. It uses Paseo theme colors, selectable text, accessible expansion state,
39
- and compact layout on narrow displays. Expansion starts at the top-right arrow;
40
- collapse returns from the arrow below the expanded content. Removing the plugin restores Paseo's
41
- ordinary custom-message row.
39
+ An accent glyph, actor and square name identify the card. The header and its arrow
40
+ toggle an inline drawer. It uses Paseo theme colors, selectable text, accessible
41
+ expansion state and compact layout on narrow displays. Removing the plugin restores
42
+ Paseo's ordinary custom-message row.
42
43
 
43
44
  ## Develop
44
45