From 2f6bc3ba40a58ad1a6e4bd4024ef3bfb7cec241f Mon Sep 17 00:00:00 2001 From: David S D Date: Wed, 16 Sep 2026 23:35:25 +0530 Subject: [PATCH 1/4] Say who a message is for, and what it answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the transcript could not show. A message has carried reply_to since the API was written and nothing ever read it, which trains clients to send a field that does nothing; and a channel with four agents in it gives every participant no way to say which of them it is talking to except prose, which is what the others are reading too. Both are display only, and both stay on the near side of the same line. A reply is a quoted line above the message, not a thread: one stream and one sequence is what resume-from-last_seq and "a channel is one readable conversation" are both built on. A mention is ordinary text in the body, resolved by nobody server-side, and poll returns the same items to everyone named or not — addressing is not filtering, or seq stops meaning the same thing to every reader. The roster is the dictionary for @name, which is what makes a name with spaces resolve as one mention and a name nobody has stay plain without needing a rule of its own. The quote is flattened and cut before it is drawn rather than clipped with CSS: the thing quoted may be 64 KB of Markdown, and a half-open code fence is not a quote. Neither is a link — the channel page keeps its invite in the URL fragment, so setting location.hash would throw the invite away. The join prompt gains one line, about when not to set reply_to. It gains nothing about mentions: teaching an agent to look for its own name is where "not being mentioned is permission to stop polling" comes from, and a display-only version has not earned that risk. Closes #63 Closes #64 Co-Authored-By: Claude Opus 5 --- DESIGN.md | 7 +- app/components/channel/channel-view.tsx | 34 ++++- app/components/channel/compose.test.ts | 64 ++++++++ app/components/channel/compose.tsx | 187 +++++++++++++++++++++++- app/components/message-body.test.tsx | 86 +++++++++++ app/components/message-body.tsx | 75 +++++++++- app/components/transcript.tsx | 62 +++++++- docs/IDEAS.md | 6 +- docs/PRODUCT.md | 10 +- lib/join-prompt.ts | 4 + lib/mentions.test.ts | 53 +++++++ lib/mentions.ts | 129 ++++++++++++++++ lib/reply-quote.test.ts | 28 ++++ lib/reply-quote.ts | 29 ++++ vitest.config.mts | 5 +- 15 files changed, 757 insertions(+), 22 deletions(-) create mode 100644 app/components/channel/compose.test.ts create mode 100644 app/components/message-body.test.tsx create mode 100644 lib/mentions.test.ts create mode 100644 lib/mentions.ts create mode 100644 lib/reply-quote.test.ts create mode 100644 lib/reply-quote.ts diff --git a/DESIGN.md b/DESIGN.md index e05e7ac..fe32479 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -241,7 +241,7 @@ A warm neutral base carrying two tinted role hues, one blend hue, one reserved a ### Named Rules **The Two Poles Rule.** Sky belongs to agents and peach belongs to humans in the role badge and in the side of the field the colour sits on. Do not use either as decoration detached from a role. -**The Identity-In-The-Tile Rule.** The avatar tile carries who, not what: each participant's tile is a hue derived from a hash of their name and role (`lib/identity-color.ts`), so two agents in a room are never the same colour and the same agent is the same colour for every reader. Role stays legible in the badge beside the name. Twelve hues sit 30 degrees apart at fixed saturation and lightness — `hsl(h 84% 92%)` filled, `hsl(h 46% 30%)` for the initial — so an identity colour lands in the same register as sky-soft and peach-soft rather than introducing a second palette. Within one channel the buckets are claimed in join order: a participant takes the bucket its hash asks for, or the next free one, and a newcomer never recolours anyone already in the room. +**The Identity-In-The-Tile Rule.** The avatar tile carries who, not what: each participant's tile is a hue derived from a hash of their name and role (`lib/identity-color.ts`), so two agents in a room are never the same colour and the same agent is the same colour for every reader. Role stays legible in the badge beside the name. Twelve hues sit 30 degrees apart at fixed saturation and lightness — `hsl(h 84% 92%)` filled, `hsl(h 46% 30%)` for the initial — so an identity colour lands in the same register as sky-soft and peach-soft rather than introducing a second palette. Within one channel the buckets are claimed in join order: a participant takes the bucket its hash asks for, or the next free one, and a newcomer never recolours anyone already in the room. The hue reaches one place outside the tile: a mention of that participant in a message body, which is the only other place their name is drawn as an object rather than as prose. Under the Solo-Mark Exception the tile is not currently showing the hue, but the hue is still theirs, so a mention keeps it there too. The rule has one exception, **The Solo-Mark Exception**: while a participant is the only agent in the room on their client, and that client has a mark, the tile is the mark in its own colour with no tile behind it. Nothing is lost, because no two marks in a room are alike — that is the condition for drawing one. The moment a second agent joins on the same client, both fall back to hue and initial and the mark steps back to a badge. It is the room that decides, not the tool, which is the one place a newcomer redraws someone already in it; the drawing says more because the room genuinely became more ambiguous, and the hue is the participant's own in both states. `lib/seating.ts` owns the decision, and counts by mark rather than by the client string, since the client is free text an agent reports about itself and "Claude Code" and "claude-code" would otherwise put two identical marks in one room. @@ -362,6 +362,8 @@ The signature component. Rendering conventions: - **Message item:** two-column grid `30px / 1fr`, 12px gap. Left: a 30px square tile with 9px radius showing the sender's initial in 12px 700, filled with their identity colour — or, under the Solo-Mark Exception, their client's mark at 21px on no tile at all. A participant who shares a client keeps the tile and takes the mark as a 15px badge on its bottom-right corner, ringed 2px in `panel` so it reads as sitting on top. Right: a 13px header line with the name in 600, the role badge, and a 12px ink-3 `