| 1 | # Slice 2 copy list |
| 2 | |
| 3 | Copy review waived for this round per suite discussion 5; catalogued for the later pass. |
| 4 | |
| 5 | This covers every user-facing string introduced on branch `slice/2-room` |
| 6 | (`77227de..6354af6`): the holding screen's scripted half, the room and its |
| 7 | client, the removal and takeover confirm pages, and the one `AGENTS.md` |
| 8 | prose change a person could trace back to a screen. Sources: `git diff |
| 9 | 77227de..6354af6 -- internal/calls/templates internal/calls/static |
| 10 | internal/calls/handlers_room.go internal/calls/handlers_join.go AGENTS.md`, |
| 11 | task-6/7/8-report.md's own "new/changed user-facing strings" lists, and the |
| 12 | reviewers' copy notes recorded in `.superpowers/sdd/2026-09-10-meet-slice-2-room/progress.md`. |
| 13 | No string below has been through `copy-review`; none was changed to write |
| 14 | this file. |
| 15 | |
| 16 | A note on the JSON refusal bodies: `stream.go`'s 410 body carries only a |
| 17 | machine reason code (`buffer`, `replaced`, `standing`, `expired`, |
| 18 | `removed`, `ended`) in `body["reason"]` — nothing in that JSON is prose a |
| 19 | person reads. `room.js`'s `terminal(reason)` looks the code up in its own |
| 20 | `TERMINAL` table to get the sentence a person actually sees, so the |
| 21 | sentences in the Terminal sentences table below are the complete set of |
| 22 | what those refusal bodies can cause to appear. |
| 23 | |
| 24 | ## Holding page (`/m/{sqid}/join`) |
| 25 | |
| 26 | `templates/join.html` |
| 27 | |
| 28 | | String | Where | Note | |
| 29 | | --- | --- | --- | |
| 30 | | This meeting is already open in another tab. | `join.html`, `content` block, `OpenElsewhere` notice | Reviewer note (progress.md, Task 6 review): untrue during the resume window — a session whose stream has merely lapsed, not collided, still sees this. Suggested replacement, not applied to code: "You are already in this meeting from this browser." | |
| 31 | | Use this tab instead | `join.html`, `content` block, link inside the `OpenElsewhere` notice, to `/m/{sqid}/takeover` | Same notice as above; this is the link text, not a separate sentence. | |
| 32 | | The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. | `join.html`, `content` block, `<noscript>` | | |
| 33 | | Your camera preview | `join.html`, `content` block, `aria-label` on `#preview` | Added in Task 7's fix round (task-7-report.md item 11). | |
| 34 | | Camera | `join.html`, `content` block, `field-select` Label for `#pick-camera` | Same word is the fixed `aria-label`/button text below, on `#toggle-camera`. | |
| 35 | | Microphone | `join.html`, `content` block, `field-select` Label for `#pick-mic` | Same word is the fixed `aria-label`/button text below, on `#toggle-mute`. | |
| 36 | | Speaker | `join.html`, `content` block, `field-select` Label for `#pick-speaker` (row hidden unless `setSinkId` is supported and a speaker is found) | | |
| 37 | | Microphone | `join.html`, `content` block, `#toggle-mute` — `aria-label` and visible button text | Task 7 fix: the accessible name is now fixed; only `aria-pressed` and the state word move. Replaces earlier "Mute"/"Unmute" wording (task-7-report.md item 10). | |
| 38 | | Camera | `join.html`, `content` block, `#toggle-camera` — `aria-label` and visible button text | Same fix; replaces earlier "Camera off"/"Camera on". | |
| 39 | | Join the call | `join.html`, `content` block, `#join` button | | |
| 40 | |
| 41 | `static/devices.js` |
| 42 | |
| 43 | | String | Where | Note | |
| 44 | | --- | --- | --- | |
| 45 | | This browser cannot reach a camera or microphone here. You can still join with neither. | `devices.js`, top level (no `navigator.mediaDevices.getUserMedia`) | "You can still join with neither" recurs in all four device-error/no-support sentences below — the room is built to allow joining with no local media, and every sentence says so the same way. | |
| 46 | | Asking for your camera and microphone… | `devices.js`, `startPreview()` | Task 7 fix: now said before the `getUserMedia` call, not only after it fails (task-7-report.md item 9). | |
| 47 | | Meet needs permission to use your camera and microphone. You can still join with neither. | `devices.js`, `errorSentence()` (`NotAllowedError`) | | |
| 48 | | Your camera or microphone is already in use by another app. You can still join with neither. | `devices.js`, `errorSentence()` (`NotReadableError`) | New in Task 7's fix round; previously fell through to the generic sentence (task-7-report.md item 8). | |
| 49 | | Meet could not reach a camera or microphone. You can still join with neither. | `devices.js`, `errorSentence()` (default — anything but `NotAllowedError`/`NotReadableError`) | Task 7 fix replaces an earlier "No camera or microphone was found…" (task-7-report.md item 8). | |
| 50 | | Muted / On | `devices.js`, `applyToggles()`, `#toggle-mute-state` | State word, never colour alone; moves with `prefs.muted`. | |
| 51 | | Off / On | `devices.js`, `applyToggles()`, `#toggle-camera-state` | State word; moves with `prefs.cameraOff`. | |
| 52 | | Camera `<n>` | `devices.js`, `labelFor()` | Fallback option label in `#pick-camera` when the browser gives no `device.label` (before permission is granted). | |
| 53 | | Microphone `<n>` | `devices.js`, `labelFor()` | Same fallback, for `#pick-mic`. | |
| 54 | |
| 55 | ## Room page (`/m/{sqid}`) |
| 56 | |
| 57 | `templates/room.html` — an entirely new file this branch. Several of its |
| 58 | lines are the holding screen's own wording, reused: noted per row. |
| 59 | |
| 60 | | String | Where | Note | |
| 61 | | --- | --- | --- | |
| 62 | | Starts {{.Starts}}. | `room.html`, `content` block (meeting has a start time) | Mirrors the holding screen's identical line (already catalogued for slice 1). | |
| 63 | | You are {{.Name}}, joining as a {{.Standing}}. | `room.html`, `content` block | Task 6 changed this from an initial "in the call as a `<kind>`." to match the holding screen's wording exactly — true before any participation exists, since this renders server-side ahead of anything `room.js` does (task-6-report.md item 14). | |
| 64 | | This team's Meet has no relay yet. Calls usually connect only between people on the same network. | `room.html`, `content` block, floor notice (no relay configured) | Mirrors the holding screen's identical notice. | |
| 65 | | The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. | `room.html`, `content` block, `<noscript>` | Same sentence as the holding screen's `<noscript>`, with a link appended (next row) the holding screen's version doesn't have. | |
| 66 | | Back to the holding screen | `room.html`, `content` block, link inside the `<noscript>`, to `/m/{sqid}/join` | Same wording `room.js`'s own `wayOut()` uses for its link of the same name (see Links, below) — the scriptless and scripted paths land on the same words. | |
| 67 | | Microphone | `room.html`, `content` block, `#mute` — `aria-label` and visible button text | Same fixed-name pattern as the holding screen's toggle. | |
| 68 | | Camera | `room.html`, `content` block, `#camera` — `aria-label` and visible button text | Same fixed-name pattern as the holding screen's toggle. | |
| 69 | | Leave | `room.html`, `content` block, `#leave` button (`rst-btn="danger"`) | | |
| 70 | | Waiting to join | `room.html`, `content` block, `#queue` heading (`h2`) | Shown only when the admission queue is non-empty. | |
| 71 | | Meetings | `room.html`, `content` block, back-nav Label (members only) | Same established back-nav label used across the app (already catalogued for slice 1); reused here, not new wording. | |
| 72 | | End the meeting | `room.html`, `content` block, member-only link to `/meetings/{sqid}/end` | Same established link text (already catalogued for slice 1); `target="_blank" rel="noopener"` is new here — the room holds the call in this tab, so ending opens in a new one. | |
| 73 | |
| 74 | `static/room.js` |
| 75 | |
| 76 | ### Status sentences (`renderStatus()`, via `say()`) |
| 77 | |
| 78 | | String | Where | Note | |
| 79 | | --- | --- | --- | |
| 80 | | The room is full. | `room.js`, `renderStatus()` | Shown while `state.full` is set, cleared by the next roster. | |
| 81 | | Waiting for someone to let you in. | `room.js`, `renderStatus()` | Shown to a guest that has knocked (joined, not yet admitted). A member in the same window is "connecting", never "waiting" — a member is admitted by its own join. | |
| 82 | | Connecting to the call. | `room.js`, `renderStatus()` | Default state between `hello` and admission, for a member or a guest that hasn't joined yet. Task 8 review flagged the risk of this sentence showing forever behind an unnamed refusal; the stall counter (`MAX_STALLS`/`STALL_WINDOW`) and the terminal sentences below are the fix. | |
| 83 | | You are the only one here. | `room.js`, `renderStatus()` | Shown once admitted with no other peers in the mesh. | |
| 84 | |
| 85 | ### Terminal sentences (`TERMINAL`, said by `terminal()`) |
| 86 | |
| 87 | | String | Where | Note | |
| 88 | | --- | --- | --- | |
| 89 | | This meeting is open in another tab. | `room.js`, `TERMINAL.replaced` | Carries two links: the takeover, which is its real next step, and "Back to the holding screen" behind it (task-10-report.md, part C). The browser gate locates the takeover by href rather than reading `#status a` as a single element. | |
| 90 | | You no longer have access to this meeting. | `room.js`, `TERMINAL.standing` | | |
| 91 | | Your invitation to this meeting has expired. | `room.js`, `TERMINAL.expired` | | |
| 92 | | Someone removed you from this meeting. | `room.js`, `TERMINAL.removed` | | |
| 93 | | This meeting has ended. | `room.js`, `TERMINAL.ended` | Also the fallback for any reason code `TERMINAL` doesn't recognise. | |
| 94 | | You cannot join this meeting. | `room.js`, `TERMINAL.gone` | Task 8 review: "the fresh-404 sentence must be neutral" — replaces an earlier "This meeting is no longer open." (progress.md Task 8 note; task-8-report.md item 1 of "New user-facing strings"). | |
| 95 | | You have been signed out. | `room.js`, `TERMINAL.signedout` | | |
| 96 | | This browser could not open the call’s connection. | `room.js`, `TERMINAL.accept` (406) | Written as `’` in the source. `TERMINAL.badlast` carried the same sentence for a 400 and was removed in the fix wave: neither probe form sends `last`, so the 400 it was written for could not reach it (review W-8). | |
| 97 | | Meet cannot reach the call. The room may be full, or you may have been signed out. | `room.js`, `TERMINAL.unreachable` | Reached after `MAX_STALLS` rounds with no `hello` inside `STALL_WINDOW` — a guess covering the two causes the client can't tell apart. | |
| 98 | | This browser cannot make calls. | `room.js`, `TERMINAL.nowebrtc` | | |
| 99 | |
| 100 | ### Links (`wayOut()`) |
| 101 | |
| 102 | | String | Where | Note | |
| 103 | | --- | --- | --- | |
| 104 | | Sign in again | `room.js`, `wayOut()`, link to `/signin` | Shown only for `signedout`, and it is the *only* link on that screen — no "Back to the holding screen" is appended, because that would just bounce the person straight back to sign-in (task-8-report.md, Deviations item 1). | |
| 105 | | Use this tab instead | `room.js`, `wayOut()`, link to `/m/{sqid}/takeover` | Shown only for `replaced`. Same wording as the takeover confirm page's own submit button (see below). | |
| 106 | | Try again | `room.js`, `wayOut()`, link to `/m/{sqid}` | Shown only for `unreachable`, first, with "Back to the holding screen" behind it. The room, not the holding screen: the guess this sentence makes is "the room may be full", and the answer to that is to try the room again. | |
| 107 | | Back to the holding screen | `room.js`, `wayOut()`, link to `/m/{sqid}/join` | Shown for every terminal reason except `signedout` — the rule is that no terminal screen is a dead end, and `replaced` is no longer the exception (task-10-report.md, part C). `signedout` still is: the holding screen would only bounce that person straight back to sign-in. | |
| 108 | |
| 109 | ### Tile captions |
| 110 | |
| 111 | | String | Where | Note | |
| 112 | | --- | --- | --- | |
| 113 | | `<name>` (you) | `room.js`, `tile()` | The self tile's caption; `<name>` is the server-rendered `data-self-name`, never typed by this browser. | |
| 114 | | Someone | `room.js`, `renderTiles()` | Fallback caption for a peer whose name hasn't arrived from the server yet. | |
| 115 | | Could not connect. | `room.js`, `tile()` | Shown on a tile whose pair has failed; never colour alone — this sentence plus `.meet-tile-note`'s weight are the two cues. | |
| 116 | | Retry | `room.js`, `tile()` | Button shown beside "Could not connect.", restarts ICE (or requests a fresh generation if the browser has no `restartIce`). | |
| 117 | | Play | `room.js`, `tile()` / `startPlaying()` | Shown only if the browser refuses autoplay on a remote track; new in Task 8's fix round (task-8-report.md item 7 / "New user-facing strings"). | |
| 118 | | Remove | `room.js`, `removeLink()` | A member's removal control on another peer's tile; opens `/m/{sqid}/remove/{peer}` in a new tab (§3.5 destructive-action rule). | |
| 119 | |
| 120 | ### The queue |
| 121 | |
| 122 | | String | Where | Note | |
| 123 | | --- | --- | --- | |
| 124 | | Someone | `room.js`, `renderQueue()` | Fallback name for a waiting guest whose name hasn't been sent. Same fallback word as the tile caption above, independent occurrence. | |
| 125 | | Admit | `room.js`, `renderQueue()` | Button beside each waiting guest's name; posts `admit` for that peer. | |
| 126 | |
| 127 | ## Removal and takeover confirm pages |
| 128 | |
| 129 | `handlers_room.go`, rendered through `templates/confirm.html` |
| 130 | |
| 131 | | String | Where | Note | |
| 132 | | --- | --- | --- | |
| 133 | | Remove `<name>` from the call? | `handlers_room.go`, `confirmRemove()` — `Title` | `<name>` comes from `hub.PeerNameFor`, the same authority `Remove` itself checks — this page can never name a peer it couldn't actually remove. Earlier draft named the meeting instead of the person (task-6-report.md, Concerns); fixed in Task 6's fix round. Applies equally to a waiting guest in the admission queue — there is no separate wording for that case (progress.md, Task 6 review note). | |
| 134 | | They will be disconnected and can rejoin only if they are still allowed in. | `handlers_room.go`, `confirmRemove()` — `Body` | One sentence for both actor kinds rather than branching copy: true for a guest, who has only the link, and for a member, who can also come back from the meeting list (code comment, `handlers_room.go`; noted there for this catalogue). | |
| 135 | | Remove | `handlers_room.go`, `confirmRemove()` — `Label` (submit button, via `confirm-form`) | | |
| 136 | | Cancel | `templates/confirm.html`, `content` block, `confirm-form` `CancelLabel` | Pre-existing text (already catalogued for slice 1); `confirmRemove`'s and `confirmTakeover`'s `CancelHref` now both point at `/m/{sqid}/join`, not back into the room — a second `/m/{sqid}` would open a second stream and trip the takeover 409 for someone who only pressed Cancel (code comment, `handlers_room.go`; issue 11 half-closed per progress.md, Task 6 review). | |
| 137 | | `<meeting title>` | `handlers_room.go`, `confirmRemove()` and `confirmTakeover()` — `BackLabel`, via `a.store.title(m)` | Back-nav label on both confirm pages, replacing the generic "Meetings" default; `confirm.html`'s `{{or .BackLabel "Meetings"}}` is what makes this possible (task-6-report.md item 9). | |
| 138 | | This meeting is open in another tab | `handlers_room.go`, `confirmTakeover()` — `Title` | | |
| 139 | | Use this tab instead? The other tab will leave the call. | `handlers_room.go`, `confirmTakeover()` — `Body` | | |
| 140 | | Use this tab | `handlers_room.go`, `confirmTakeover()` — `Label` (submit button) | Distinct from `room.js`'s "Use this tab instead" link text (`wayOut()`) and the holding screen's identical link text — three slightly different wordings for the same move, not yet reconciled. | |
| 141 | |
| 142 | ## The `AGENTS.md` exception line |
| 143 | |
| 144 | | String | Where | Note | |
| 145 | | --- | --- | --- | |
| 146 | | Screens work with JavaScript disabled, with one exception: browser media participation (the holding screen's camera preview and join button, and the call itself in the room) needs script; both pages render their title, standing, floor notice and navigation without it. Destructive actions get their own confirm-page URL. | `AGENTS.md`, "Rules the family holds hard" | Replaces the prior unqualified "Screens work with JavaScript disabled; destructive actions get their own confirm-page URL." — the exception is scoped to exactly the two pages above. | |
| 147 | | `make browser` is the second gate, for any change to a page or to the client: a real call in real Chromium. It needs the Claude Code bash sandbox **off** — the sandbox blocks UDP, so Chromium gathers no ICE candidates and every call fails with `frames` stuck at 0, which reads exactly like a client bug and is not one. | `AGENTS.md`, "Rules the family holds hard", new bullet | Developer-facing (this doc, and whoever else reads `AGENTS.md`), not end-user copy, but a string introduced on this branch a person reads. | |
| 148 | | Accessible by construction, progressively enhanced, and never dependent on JavaScript to be usable, with the one exception named above: browser media participation, on the holding screen and in the room. | `AGENTS.md`, "UX conventions" intro line | Same exception restated where the UX-conventions section makes the blanket claim; kept consistent with the Rules-section wording rather than contradicting it (task-6-report.md item 10). | |
| 149 | |
| 150 | ## The Codex round 1 fix wave |
| 151 | |
| 152 | **No new user-facing string.** The wave (`3c8c6ac..`, recorded in |
| 153 | `docs/superpowers/reviews/2026-09-10-slice-2-record.md` §2a) changed |
| 154 | what the server and the client *do* and nothing a person reads: the |
| 155 | refusals it adds are the existing 404 body and the existing `TERMINAL` |
| 156 | sentences, reached by more paths than before. Two of those sentences |
| 157 | are now reachable for a new reason, and the wording is unchanged: |
| 158 | |
| 159 | | String | Where | Note | |
| 160 | | --- | --- | --- | |
| 161 | | You no longer have access to this meeting. | `room.js`, `TERMINAL.standing` | Already catalogued. Now also reached without waiting for the tick: a removed member's next relay, reset or resume is refused, and the probe that follows a signal 404 reads the `standing` tombstone (spec §15.12). | |
| 162 | | This meeting is already open in another tab. | `join.html`, `OpenElsewhere` notice | Already catalogued, with the standing note that it is untrue during the resume window. One of the two cases behind that note is gone: a participation past its window no longer counts as open elsewhere (S2-2). The suggested rewording still stands for the rest. | |
| 163 | |
| 164 | One developer-facing string is new, in a code comment and a `Makefile` |
| 165 | comment rather than on a screen, and is listed only so this file stays a |
| 166 | complete record of the branch: |
| 167 | |
| 168 | | String | Where | Note | |
| 169 | | --- | --- | --- | |
| 170 | | meet: reset | `room.js`, `requestReset()` — `console.error` prefix | Diagnostic, in the same shape as the file's other two (`meet: pageshow`, `meet: start`). Never rendered. | |
| 171 | |
| 172 | ## The Codex round 2 fix wave |
| 173 | |
| 174 | **No new user-facing string.** The round-2 wave (`de597c6..`, recorded |
| 175 | in the review record's §2b) changed when the client tears a call down |
| 176 | and what a probe is allowed to write. Nothing new is rendered, and no |
| 177 | existing sentence changed. One row is worth restating because the wave |
| 178 | narrowed what can reach it: |
| 179 | |
| 180 | | String | Where | Note | |
| 181 | | --- | --- | --- | |
| 182 | | This meeting is open in another tab. | `room.js`, `TERMINAL.replaced` | Already catalogued. Two ways of reaching it wrongly are now closed: a transient probe answer no longer sends the client round the fresh-entry ladder into its own participation (S2-12), and neither does a 404 about a peer that merely left (S2-3, round 1). It is now shown only when this session really does hold a peer somewhere else. | |
| 183 | |
| 184 | ## Slice 3 |
| 185 | |
| 186 | The relay slice (`docs/superpowers/plans/2026-09-10-meet-slice-3-relay.md`). |
| 187 | Copy review is waived this round per suite discussion 5; every new string |
| 188 | is catalogued here instead. Four new strings are user-facing, all in |
| 189 | `room.js`; two strings already in this file are listed beside them, |
| 190 | because slice 3 changes when one of them is shown and adds a second |
| 191 | path to the other. |
| 192 | |
| 193 | | String | Where | Note | |
| 194 | | --- | --- | --- | |
| 195 | | Meet could not reach the relay. Calls may not connect until it can. | `room.js`, `iceWarning()` — `#ice-warning`, a `role="status"` paragraph the script creates once beside `#status` | Spec §5.3 step 1. Shown from the first failed `/ice` fetch until one succeeds; the client retries every minute behind it. Its own live region rather than `#status`, so it cannot take the call's own sentence with it. Says what happened and what it may cost, and does not diagnose the network — §5.5's rule. **Shown only where a relay is configured** (`data-relay`): on the floor there is no relay this instance could reach, and the sentence would be untrue. A 429 is not a failure and never shows it: the server named a retry time and the client waits it out inside the same budget. | |
| 196 | | The relay credential expired for this connection. | `room.js`, `tileNote()` — a tile's `.meet-tile-note` | Spec §5.3 step 3. Shown per connection, not per call: the must-succeed-by is the old credential's expiry, and the browser cannot see which credential an allocation is using, so this is the honest signal and not a diagnosis. The existing "Retry" beside it runs a fresh fetch and then another ICE restart. | |
| 197 | | Could not renew this connection through the relay. | `room.js`, `tileNote()` — a tile's `.meet-tile-note` | Spec §5.3 step 2's other ending, and the reason it is not the sentence above. Three ICE restarts with a *good* new credential in hand, none of them proven within thirty seconds: the credential is fine and the path is not, so "expired" would be a sentence about the wrong thing. Same Retry beside it. Codex round 1 (S3-4) tightened which of the two a person sees: with a replacement credential in hand this is now the *only* possible sentence, where the old deadline could previously rename it "expired" after a single attempt. | |
| 198 | | Could not connect, even through the relay. | `room.js`, `tileNote()` | Spec §5.5, in the spec's own words. Replaces "Could not connect." on a failed tile when `#room` carries `data-relay="1"` **and this connection was actually given the list** — a connection still running on the empty list never went near a relay, and the sentence would be a claim about something it never tried. The README's TCP-443-only note (slice 6) is where the possible causes are named; the tile does not diagnose. | |
| 199 | | Retry | `room.js`, `tile()` | Already catalogued for the failed tile. Now also the control on an expired tile, where it means "fetch a new credential and restart", not "restart". One label, because from a person's side it is one thing: try this connection again. | |
| 200 | | Could not connect. | `room.js`, `tileNote()` | Already catalogued. Unchanged, and still what a failed tile says on an instance with no relay (§5.5's floor). | |
| 201 | |
| 202 | Developer-facing, in `console.error` prefixes rather than on a screen, |
| 203 | listed only so this file stays a complete record of the branch. All |
| 204 | three print an error object and never a response body: the `/ice` |
| 205 | answer carries a credential, and nothing about the servers reaches the |
| 206 | console. |
| 207 | |
| 208 | | String | Where | Note | |
| 209 | | --- | --- | --- | |
| 210 | | meet: ice | `room.js`, the one-minute retry's rejection handler | Same shape as `meet: reset`, `meet: pageshow`, `meet: start`. Never rendered. | |
| 211 | | meet: renew | `room.js`, the renewal's rejection handlers | As above. | |
| 212 | | meet: roster | `room.js`, `onRoster()` — the roster path is asynchronous now that §5.2's fetch is in front of the first connection | As above. | |
| 213 | |
| 214 | ## Slice 4 |
| 215 | |
| 216 | The chat and screen-share slice |
| 217 | (`docs/superpowers/plans/2026-09-10-meet-slice-4-chat.md`). Copy review |
| 218 | is waived this round per suite discussion 5; every new string is |
| 219 | catalogued here instead. |
| 220 | |
| 221 | ### The chat panel |
| 222 | |
| 223 | `internal/calls/templates/partials/chat.html` and |
| 224 | `internal/calls/static/chat.js` — the suite's first chat panel (spec §6, |
| 225 | §10). These five strings belong to the panel and not to Meet: they ship |
| 226 | with the partial and the script, and a second app that adopts those two |
| 227 | files adopts these words with them. That is the reason to look hard at |
| 228 | them now — a sentence changed after a second app has taken it is a |
| 229 | sentence changed in two products. |
| 230 | |
| 231 | | String | Where | Note | |
| 232 | | --- | --- | --- | |
| 233 | | Chat | `chat.html`, the panel's `<h2 class="chat-title">` and its `aria-label` | The panel's name, in the heading and again on the section so the region is announced by name. One word, and the same word twice on purpose: a label that disagrees with the visible heading is worse than either alone. | |
| 234 | | Message | `chat.html`, the `<label for="chat-text">` | The control's label. Not "Your message" and not a placeholder: a label that only exists until you type is not a label. | |
| 235 | | Send | `chat.html`, the submit button | The verb, not "Submit". | |
| 236 | | Could not send. Try again. | `chat.js`, `#chat-status` — a `role="status"` paragraph inside the form | Shown whenever the host's `send` answers anything but exactly `true`, throwing included: the message was not accepted, so nobody will ever see it and nothing was echoed into the log. The draft stays in the box, which is what "Try again" means here — press Send again, not retype it. Editing the draft clears the sentence, because it was about text that no longer exists. Says what happened and does not diagnose the network (§5.5's rule, applied to chat). | |
| 237 | | Message is too long. | `chat.js`, `#chat-status` | Shown while the textarea holds more than 2048 **bytes** of the text as the wire carries it (§4.4) — its JSON-escaped UTF-8 form, which is what the server measures too, so a quote, a backslash and a newline each cost two — and the Send button is disabled behind it. The control's own `maxlength="2048"` counts characters, so for anything outside ASCII, and for anything JSON escapes, the browser will happily allow a message the server would refuse with a 413; this sentence is what stands in that gap. (The string is unchanged; the measure behind it was corrected in the Codex fix wave, W4-1 — before that the panel counted raw UTF-8 and the server counted raw JSON, and a draft with two dozen quotes in it fell between them.) Present tense because it describes the box as it is right now, and it clears itself when the draft comes back under the limit. | |
| 238 | |
| 239 | Two strings on a chat line are generated, not authored, and are listed |
| 240 | only so this file stays a complete record: the visible clock (`HH:MM`, |
| 241 | the reader's own local time) and the `title` on the same `<time>` |
| 242 | element, which is `Date.prototype.toLocaleString()` — the whole date and |
| 243 | time in the reader's own locale and wording, per AGENTS.md's dates |
| 244 | convention. Neither is text anyone here wrote, and neither is |
| 245 | translatable by this app. |
| 246 | |
| 247 | Neither sentence is carried by colour: `.chat-status` paints in |
| 248 | `--rst-text`, the ordinary text colour, and the weight it adds is a |
| 249 | second cue on top of the words rather than the signal itself |
| 250 | (AGENTS.md, "State — never colour alone"). |
| 251 | |
| 252 | ### The room: chat and screen share |
| 253 | |
| 254 | `internal/calls/static/room.js`, `internal/calls/templates/room.html` — |
| 255 | the room's own half of the slice. These strings are Meet's, not the |
| 256 | panel's: they name a control in this room and a thing that happened in |
| 257 | this call. |
| 258 | |
| 259 | | String | Where | Note | |
| 260 | | --- | --- | --- | |
| 261 | | Share screen | `room.html`, `#share` in `#controls` | The verb and its object, because "Share" alone in a call could be a link, a file or a screen. The name never changes under the reader: the state moves in `aria-pressed` and in the aria-hidden span beside it, so the button is announced as the same control whether it is on or off — the rule the Microphone and Camera toggles already follow. The two device toggles carry the name again in `aria-label`; this one does not need to, because its visible text *is* the name and the only part that changes is hidden from the reader. | |
| 262 | | On | `room.js`, `renderShare()` — `#share-state` | The word in the button while this tab is sharing. Same word as the other two toggles use for the same idea, on purpose. Never the whole signal: `aria-pressed` and calls.css's inset outline say it twice more, and neither is colour. | |
| 263 | | Off | `room.html` and `room.js`, `renderShare()` — `#share-state` | The resting state, rendered by the server so a screen shows the truth before the script runs, and restored by `renderShare()` when sharing stops. | |
| 264 | | *<name>*’s screen | `room.js`, `shareCaption()` — the `<figcaption>` of a share tile | The caption on the second tile a sharing peer gets. The name is the roster's, written with `textContent` like every other name in the room, and the apostrophe is the typographic one (U+2019) the rest of this app uses. A screen is captioned by whose it is, because in a grid of faces that is the only thing that tells two screens apart. Not "Screen" and not "Shared screen": both are true of every tile in the row. | |
| 265 | | Screen sharing was not allowed. | `room.js`, `startSharing()` — `#status`, the room's own `role="status"` line | Shown when `getDisplayMedia` refuses: the person dismissed the browser's picker, or a policy forbade the capture. Past tense and passive on purpose — the browser refused, and this file cannot see which of the two it was, so it does not guess. It does not say "try again", because the button is still there and still says what it does. Cleared by the next thing the room has to say, like every other sentence on that line. | |
| 266 | |
| 267 | Developer-facing, in a `console.error` prefix rather than on a screen: |
| 268 | |
| 269 | | String | Where | Note | |
| 270 | | --- | --- | --- | |
| 271 | | meet: share | `room.js`, the share button's rejection handler | Same shape as `meet: start`, `meet: roster`, `meet: renew`. Never rendered, and it prints an error object — never a capture, a track id or a peer's name. | |
| 272 | | meet: chat | `room.js`, `runBroadcast()` — the broadcast lane's rejection handler | As above. Nothing awaits the pump, so a rejection escaping it would go where neither a person nor a gate could see it. It prints an error object and never a message: §4.4 says nothing of an accepted chat reaches a log line, and that includes this one. | |
| 273 | |
| 274 | ## Slice 5 |
| 275 | |
| 276 | The drive surface's Task 2, Settings: mint an agent's token once, show |
| 277 | it, and revoke it |
| 278 | (`.superpowers/sdd/2026-09-11-meet-slice-5-drive/task-2-brief.md`). |
| 279 | Every string below is new; none replaces or renames a string already |
| 280 | in this file. |
| 281 | |
| 282 | | String | Where | Note | |
| 283 | | --- | --- | --- | |
| 284 | | Settings | `templates/layout.html`, the account corner beside sign-out; `templates/settings.html`'s `<title>` and `page-header` | The screen's name, in the nav link and again as the page's own title — the same doubling `link_once.html`'s "Your meeting link" and index's "Meetings" already use between a page's `<title>` and its heading. Shown in the shell only when `Viewer.Member` is true (`render.go`'s `page`, now a method so it can ask `a.isMember`): a guest signed into a room they were let into never sees it, because they have no agents to manage. | |
| 285 | | Agents | `settings.html`, the list section's `<h2>` | The section's name, plural because the screen is the list of them. Not repeated as a page title — "Settings" already owns that. | |
| 286 | | An agent acts on your behalf with exactly your reach, as *<name>*~*<your address>*. | `agent_once.html`, the sentence under the minted agent's name | Spec §8.1's principal, spelled out in one sentence rather than left as a bare `name~email` string: says what an agent *is* (a delegate with the operator's own reach, no more and no less) at the one moment a person is looking straight at the credential that makes it true. `~` between the two halves is deliberate and unexplained here — it is the same separator `principal()` (`agents.go`) writes into a meeting's creator field, so a person who later sees `scheduler~paul@rslw.com` on a meetings list recognises the shape from this sentence. | |
| 287 | | Name | `settings.html`, the create form's field label (`ui/field`) | The one field the create form has. Not "Agent name": the field envelope is already inside a "Create an agent" section: heading, then label, would be typed as "Create an agent" / "Agent name" / an agent name — this is `title.html`'s "Title" pattern (a form under its own heading names its field plainly, not by repeating the heading's noun). | |
| 288 | | Create an agent | `settings.html`, the creation section's `<h2>` and its submit button | One string in two places on purpose, the way `chat.html`'s "Chat" names both a heading and an `aria-label` for the same reason: a button whose action reads differently from the section it sits in is a small tax on every reader, paid for nothing. Says what happens, not "New" or "Add" (`page-header.html`'s own rule for `ActionLabel`, applied here to a plain form button instead). | |
| 289 | | This is the only time this token is shown. Copy it now. | `agent_once.html`, the paragraph above the readonly token field | `link_once.html`'s "This is the only time this link is shown. Copy it now and send it to the people you are meeting." shortened to fit a token instead of a link: nobody is sent a token the way a meeting link is sent to attendees, so the sentence stops after "Copy it now." rather than inventing an audience for it. | |
| 290 | | Revoke | `settings.html`, the link on each live agent's row; `handlers_settings.go`'s confirm page, the destructive button (`ui/confirm-form`'s `Label`) | The verb, matching "Replace the link" / "End" on the meetings list: an action link named for what it does, and the same word again as the confirm page's own submit — one control, described consistently on the way to it and on it. | |
| 291 | | Revoke *<name>*? Anything using its token stops working at once. | `handlers_settings.go`'s `confirmRevokeAgent`, split as `confirm.html`'s `Title` ("Revoke *<name>*?") and `Body` ("Anything using its token stops working at once.") | Same two-part shape as "End the meeting?" / "'*<title>*' will close for everyone and its link will stop working." (`handlers_meetings.go`'s `confirmEnd`): the question names the thing by its own identity (the agent's name, not "this agent"), the body says the one consequence that matters — every request the token would have authorized now fails, immediately, not at some later sweep. "At once" carries that timing; nothing here promises what already-open work using the token will see, because nothing in this slice tracks that. | |
| 292 | | Revoked | `settings.html`, a revoked row (`ui/status-pill`, default neutral tone) | The one-word state for a row that used to show its principal. A pill, not a link, because there is no action left to take from here — spec's "state is never colour alone" (AGENTS.md): the tone is neutral and the word is what actually carries the meaning. | |
| 293 | | No agents yet. | `settings.html`, the empty list (`ui/empty-state`) | Deliberately terser than "No meetings yet. Make one above and share its link." (`index.html`'s own empty state): an agent is a less familiar idea than a meeting, and this screen's create form sits directly under the heading with no other section between — the empty state does not need to teach what the form immediately beneath it already explains at a glance. | |
| 294 | | Give the agent a name of 1 to 64 letters, digits, - or _. | `handlers_settings.go`'s `createAgentHandler`, beside the Name field on `errAgentName` | Spells out the rule `agentNameRe` (`agents.go`) enforces, the same register as "Keep the title under 200 characters." — the limit stated as a fact about the field, not a complaint about what was typed. The submitted name is never echoed back inside this sentence (only into the field's own `Value`, `ui/field`'s ordinary redisplay), so a name containing something worth not repeating verbatim in a message never has to be. | |
| 295 | | You already have 20 live agents. Revoke one before creating another. | `handlers_settings.go`'s `createAgentHandler`, beside the Name field on `errAgentLimit` | The refusal for the twenty-live-agent ceiling the Task 1 review asked for. States the number and the way out, the same shape as the sign-in form's "Too many sign-in attempts just now. Wait a few minutes and try again." As of fix round 1, `store.createAgent` itself counts live agents (inside the same transaction as the insert) and returns `errAgentLimit` at twenty; the handler only renders the sentence — see `agents.go` and the task's report. | |
| 296 | | You already have a live agent with that name. | `handlers_settings.go`'s `createAgentHandler`, beside the Name field on `errAgentDuplicate` | As of fix round 1, `store.createAgent` checks for a live duplicate name itself (a plain count query in the same transaction, no unique index — a revoked name is free to reuse at once) and returns `errAgentDuplicate`; the handler only renders the sentence. See `agents.go` and the task's report. | |
| 297 | |
| 298 | Two sentinel errors, `errAgentLimit` and `errAgentDuplicate`, live in |
| 299 | `agents.go` beside `errAgentName` as of fix round 1 — see the task's |
| 300 | report for the move from `handlers_settings.go`, where they were |
| 301 | declared as scaffolding for this task's duration only. |
| 302 | |
| 303 | Task 4, the badge on the list |
| 304 | (`.superpowers/sdd/2026-09-11-meet-slice-5-drive/task-4-brief.md`): |
| 305 | `index.html` names which meetings an agent made, right beside every |
| 306 | other meeting a member made by hand. |
| 307 | |
| 308 | | String | Where | Note | |
| 309 | | --- | --- | --- | |
| 310 | | Agent | `index.html`, `{{template "badge" dict "Label" "Agent"}}` beside an agent-made meeting's title, both the Upcoming and Ended lists | The one word, in `ui/badge` (AGENTS.md's vendored component — not hand-rolled) beside the lucide `bot` icon (`internal/calls/icons`): the icon alone would fail AGENTS.md's "state is never colour alone" rule on its own account (a shape is not a colour, but it is still not text), so the badge's own text label is what actually says what the icon means, exactly as `ui/badge`'s doc comment expects it to be used. Shown only when the row's `ByAgent` is true (`store.meetingViewOf`) — a member's own meeting carries neither the icon nor the word. | |
| 311 | | by *<principal>* | `index.html`, the `<p>` directly under an agent-made meeting's title | Spec §8.1's "robot badge and operator name on every write", the second half of it: the principal a person already met on Settings' `agent_once.html` ("An agent acts on your behalf with exactly your reach, as *<name>*~*<your address>*.") and would recognise here without translation — `claude~paul@example.com` is the same shape, resolved by `store.creatorLabel` through the same `principal()` (`agents.go`). Lowercase "by" because it reads as a caption under a title, not a sentence of its own; present only when the row is an agent's. | |
| 312 | |
| 313 | `meetingView.CreatedByPrincipal` also resolves a member-made meeting's |
| 314 | creator to their display name (`store.creatorLabel`'s `person:<id>` |
| 315 | branch), but nothing on `index.html` shows that value: a member does |
| 316 | not need to be told they made their own meeting, and spec §8.1's |
| 317 | "on every write" is about the drive surface's writes, not every write. |
| 318 | The field exists on every row; the template reads it only when |
| 319 | `ByAgent` is true. |
| 320 | |