Repository navigation
Guest page: the guest's composer and its own messages (carries #26819) - #30518
Conversation
Part 2 of the guest page (design/chat-multiplayer.md sections 3 and 4.3, manaflow-ai/cmuxterm-hq#4766). - Composer: text only (no attachments, slash commands or pickers); Return sends, Shift-Return a new line. A message over 32 KiB (UTF-8) cannot be sent and says so. - Each message is `prompt {client_id, text}`. The client id is 128 random bits made when the message is written and kept with it in the tab's sessionStorage (guestPrompts.ts), so a resend is the same prompt: a message the room never acknowledged goes again, same id, with the next snapshot. - The guest's own messages: while waiting, its chip, the text and "Sending…" then "Waiting for <owner>"; decided, one quiet line each: "Sent: …", "Declined: …", "Withdrawn: …", "Not sent: …", and for share.too_many_held "Not sent, too many messages waiting: …". States come from prompt_state and the snapshot's prompts; the snapshot asks prompt_status for held ones it does not answer. - Typing (typingSignal.ts): on at the first keystroke, off on send, clear or 5 s idle, at most two frames a second, one-shot timers only; a reconnect re-sends the current state. - Gallery guest-page.waiting (held, decided). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c3300437b0af7f1bba36 (cherry picked from commit 8212e5e)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c3300437b0af7f1bba36 (cherry picked from commit a3ef8f7)
…prop) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c3300437b0af7f1bba36 (cherry picked from commit 5cde0f9)
|
Review (subagent claude-opus-5-5 medium, independent): SHIP at 2f3724b No blocking findings. Guest text and names render as React text (no HTML injection); the page sends only Follow-ups (tracked in an issue):
|
There was a problem hiding this comment.
5 issues found across 18 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="webviews/src/agent-session/guest/redeem.ts">
<violation number="1" location="webviews/src/agent-session/guest/redeem.ts:72">
P3: The added `joinedName` declaration captures the existing `rememberJoined` JSDoc, so `joinedName` is documented as if redeeming succeeded and `rememberJoined` loses its documentation. Move the name getter (and its comment) before the redeem comment, or place the redeem comment directly above `rememberJoined`.</violation>
</file>
<file name="webviews/test/guest-prompts.test.ts">
<violation number="1" location="webviews/test/guest-prompts.test.ts:50">
P2: This retention test checks only the count and final item, so it can pass with old decided messages retained and newer ones dropped. Assert the complete retained sequence to verify that `decided 4` through `decided 19` remain.</violation>
</file>
<file name="webviews/src/agent-session/guest/guest.css">
<violation number="1" location="webviews/src/agent-session/guest/guest.css:27">
P2: The non-shrinking dock can exceed the viewport on short phone or keyboard-resized views, leaving the composer or Send button below the clipped page. Let the dock/message list shrink or provide an internal scroll path so the composer remains reachable.</violation>
</file>
<file name="webviews/src/agent-session/guest/GuestComposer.tsx">
<violation number="1" location="webviews/src/agent-session/guest/GuestComposer.tsx:38">
P2: The decided rows render above waiting rows, putting older status summaries before actionable pending prompts. Render `open` before `decided` to match the documented guest-message order.</violation>
</file>
<file name="webviews/src/agent-session/guest/GuestPage.tsx">
<violation number="1" location="webviews/src/agent-session/guest/GuestPage.tsx:222">
P3: Guests with an existing session that has no stored name show their opaque guest ID instead of a usable name in the own-message chip. Pass the localized fallback into `participant()` before it can fall back to the ID.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| } | ||
| const list = prompts.list(); | ||
| expect(list[0]!.clientId).toBe(waiting.clientId); | ||
| expect(list.length).toBe(17); |
There was a problem hiding this comment.
P2: This retention test checks only the count and final item, so it can pass with old decided messages retained and newer ones dropped. Assert the complete retained sequence to verify that decided 4 through decided 19 remain.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/test/guest-prompts.test.ts, line 50:
<comment>This retention test checks only the count and final item, so it can pass with old decided messages retained and newer ones dropped. Assert the complete retained sequence to verify that `decided 4` through `decided 19` remain.</comment>
<file context>
@@ -0,0 +1,57 @@
+ }
+ const list = prompts.list();
+ expect(list[0]!.clientId).toBe(waiting.clientId);
+ expect(list.length).toBe(17);
+ expect(list.at(-1)!.text).toBe("decided 19");
+});
</file context>
| expect(list.length).toBe(17); | |
| expect(list.map((each) => each.text)).toEqual([ | |
| "waiting", | |
| ...Array.from({ length: 16 }, (_, index) => `decided ${index + 4}`), | |
| ]); |
| .guest-live__ask-title{color:var(--agent-muted);font-family:var(--font-mono);overflow-wrap:anywhere} | ||
| .guest-live__transcript{flex:1;min-height:0;display:flex;flex-direction:column} | ||
| /* The dock under the chat: the guest's own messages, then its composer (GuestComposer.tsx). */ | ||
| .guest-live__dock{flex:none;display:flex;flex-direction:column;gap:8px;padding:8px var(--guest-inset) 12px;border-top:1px solid var(--agent-border)} |
There was a problem hiding this comment.
P2: The non-shrinking dock can exceed the viewport on short phone or keyboard-resized views, leaving the composer or Send button below the clipped page. Let the dock/message list shrink or provide an internal scroll path so the composer remains reachable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/guest/guest.css, line 27:
<comment>The non-shrinking dock can exceed the viewport on short phone or keyboard-resized views, leaving the composer or Send button below the clipped page. Let the dock/message list shrink or provide an internal scroll path so the composer remains reachable.</comment>
<file context>
@@ -22,3 +23,14 @@
.guest-live__ask-title{color:var(--agent-muted);font-family:var(--font-mono);overflow-wrap:anywhere}
.guest-live__transcript{flex:1;min-height:0;display:flex;flex-direction:column}
+/* The dock under the chat: the guest's own messages, then its composer (GuestComposer.tsx). */
+.guest-live__dock{flex:none;display:flex;flex-direction:column;gap:8px;padding:8px var(--guest-inset) 12px;border-top:1px solid var(--agent-border)}
+[data-borders="none"] .guest-live__dock{border-top:0}
+.guest-mine{display:flex;flex-direction:column;gap:8px;max-height:40vh;overflow-y:auto}
</file context>
| {decided.map((prompt) => ( | ||
| <p key={prompt.clientId} className="guest-mine__decided" data-state={prompt.state}> | ||
| {t( | ||
| prompt.reason === "share.too_many_held" | ||
| ? "guest.prompt.tooManyHeld" | ||
| : DECIDED_TEXT[prompt.state as "approved"], | ||
| { | ||
| text: firstLine(prompt.text), | ||
| }, | ||
| )} | ||
| </p> | ||
| ))} | ||
| {open.map((prompt) => ( | ||
| <div key={prompt.clientId} className="cv-user guest-mine__held" data-state={prompt.state}> | ||
| <AuthorChip author={me} /> | ||
| <div className="ui-glass cv-user__bubble selectable">{prompt.text}</div> | ||
| <span className="guest-mine__label"> | ||
| {prompt.state === "sending" ? t("guest.prompt.sending") : t("guest.prompt.waiting", { owner })} | ||
| </span> | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
P2: The decided rows render above waiting rows, putting older status summaries before actionable pending prompts. Render open before decided to match the documented guest-message order.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/guest/GuestComposer.tsx, line 38:
<comment>The decided rows render above waiting rows, putting older status summaries before actionable pending prompts. Render `open` before `decided` to match the documented guest-message order.</comment>
<file context>
@@ -0,0 +1,117 @@
+ if (!open.length && !decided.length) return null;
+ return (
+ <section className="guest-mine" aria-live="polite">
+ {decided.map((prompt) => (
+ <p key={prompt.clientId} className="guest-mine__decided" data-state={prompt.state}>
+ {t(
</file context>
| {decided.map((prompt) => ( | |
| <p key={prompt.clientId} className="guest-mine__decided" data-state={prompt.state}> | |
| {t( | |
| prompt.reason === "share.too_many_held" | |
| ? "guest.prompt.tooManyHeld" | |
| : DECIDED_TEXT[prompt.state as "approved"], | |
| { | |
| text: firstLine(prompt.text), | |
| }, | |
| )} | |
| </p> | |
| ))} | |
| {open.map((prompt) => ( | |
| <div key={prompt.clientId} className="cv-user guest-mine__held" data-state={prompt.state}> | |
| <AuthorChip author={me} /> | |
| <div className="ui-glass cv-user__bubble selectable">{prompt.text}</div> | |
| <span className="guest-mine__label"> | |
| {prompt.state === "sending" ? t("guest.prompt.sending") : t("guest.prompt.waiting", { owner })} | |
| </span> | |
| </div> | |
| ))} | |
| {open.map((prompt) => ( | |
| <div key={prompt.clientId} className="cv-user guest-mine__held" data-state={prompt.state}> | |
| <AuthorChip author={me} /> | |
| <div className="ui-glass cv-user__bubble selectable">{prompt.text}</div> | |
| <span className="guest-mine__label"> | |
| {prompt.state === "sending" ? t("guest.prompt.sending") : t("guest.prompt.waiting", { owner })} | |
| </span> | |
| </div> | |
| ))} | |
| {decided.map((prompt) => ( | |
| <p key={prompt.clientId} className="guest-mine__decided" data-state={prompt.state}> | |
| {t( | |
| prompt.reason === "share.too_many_held" | |
| ? "guest.prompt.tooManyHeld" | |
| : DECIDED_TEXT[prompt.state as "approved"], | |
| { | |
| text: firstLine(prompt.text), | |
| }, | |
| )} | |
| </p> | |
| ))} |
|
|
||
| /** The redeem worked: the secret and the attempt are spent, and the tab remembers it joined. */ | ||
| export function rememberJoined(shareId: string, guestId: string, storage: Storage): void { | ||
| /** The name this tab joined the share with. */ |
There was a problem hiding this comment.
P3: The added joinedName declaration captures the existing rememberJoined JSDoc, so joinedName is documented as if redeeming succeeded and rememberJoined loses its documentation. Move the name getter (and its comment) before the redeem comment, or place the redeem comment directly above rememberJoined.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/guest/redeem.ts, line 72:
<comment>The added `joinedName` declaration captures the existing `rememberJoined` JSDoc, so `joinedName` is documented as if redeeming succeeded and `rememberJoined` loses its documentation. Move the name getter (and its comment) before the redeem comment, or place the redeem comment directly above `rememberJoined`.</comment>
<file context>
@@ -69,10 +69,14 @@ export function attemptId(
/** The redeem worked: the secret and the attempt are spent, and the tab remembers it joined. */
-export function rememberJoined(shareId: string, guestId: string, storage: Storage): void {
+/** The name this tab joined the share with. */
+export const joinedName = (shareId: string, storage: Storage) => storage.getItem(key(shareId, "name")) ?? undefined;
+
</file context>
|
|
||
| const owner = ownerOf(people); | ||
| const ownerName = owner?.name ?? t("guest.live.owner"); | ||
| const me = participant(guestId, people.find((person) => isMe(person.id, guestId))?.name ?? name) ?? { |
There was a problem hiding this comment.
P3: Guests with an existing session that has no stored name show their opaque guest ID instead of a usable name in the own-message chip. Pass the localized fallback into participant() before it can fall back to the ID.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At webviews/src/agent-session/guest/GuestPage.tsx, line 222:
<comment>Guests with an existing session that has no stored name show their opaque guest ID instead of a usable name in the own-message chip. Pass the localized fallback into `participant()` before it can fall back to the ID.</comment>
<file context>
@@ -203,6 +219,16 @@ export function LiveChat({
const owner = ownerOf(people);
const ownerName = owner?.name ?? t("guest.live.owner");
+ const me = participant(guestId, people.find((person) => isMe(person.id, guestId))?.name ?? name) ?? {
+ id: guestId,
+ name: t("guest.you"),
</file context>
| const me = participant(guestId, people.find((person) => isMe(person.id, guestId))?.name ?? name) ?? { | |
| const me = participant(guestId, people.find((person) => isMe(person.id, guestId))?.name ?? name ?? t("guest.you")) ?? { |
|
Found 18 test failures on Blacksmith runners: Failures
...and 8 more test failures. View all on Blacksmith: run 41287673764, run 41287673623
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a red: cmux-next checks (god files, concurrency, crash safety, l10n); base red: the same failure on the base bcaf263; does not block Shipped in:
|
|
SW-12 integration warning for this guest-composer PR (exact head |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo's words (2026-10-28), on Theo's T3 Code "Share with guest": "we want to be having, be able to do this kind of flow for CMux multiplayer ... we can work on the agent chat multiplayer." In that flow each message the guest sends waits for the owner. And today: "eh i think we'd better try to get at least the agent sessions' group chats in and refined".
This carries #26819 (part 2 of the guest page) onto current
feat-cmux-next, now that part 1 landed as #29901 (6e5c3a5). It has only #26819's own three commits, cherry-picked with their authorship kept; #26779's commits are already on base through #29901. All three applied cleanly, so no line here differs from #26819.8212e5eGuest page: the guest's composer and its own messages (GuestComposer.tsx,guestPrompts.ts,typingSignal.ts, the waiting gallery and tests).a3ef8f7Guest page: text fields show focus by their edge, not a ring.5cde0f9test: the guest page's spy test gives the page its storage (part 2's prop).Plan: manaflow-ai/cmuxterm-hq#4766. Spec: leo-workspace
design/chat-multiplayer.md, section 3 (the guest's composer) and section 4.3 (prompt,typing,prompt_state,prompt_status).The owner's side of a held prompt (Approve / Decline) is the held-prompt stack (#28859, #29003, #26862, #26742), owned by another lane; until it lands a guest's prompt shows as waiting.
#26819 can close once this lands.
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Adds the guest page's composer so a guest can send messages to the agent, while the chat's owner approves or declines each one before the agent sees it.
sessionStorage, so a resend after a dropped connection or a reload is the same prompt.Written for commit 2f3724b. Summary will update on new commits.