Public: anyone can read this; the crew can change it.
A phone share sheet reaches the crew in one tap: the share target is tested from a shared URL to a queued thought, and a signed-out sharer is brought back to it #10417
Check · Review · Land · Live done — Landed and live in production.
A phone share now opens the home tell box filled in. A shared draft posted twice queues one thought. Return after sign-in was already in place, and I tested it. `test/share-target.test.ts` passes (5 tests) and `tsc`, the gate and design-lint are clean.
shipped production is serving it deploy 9e8c29c
A phone share now opens the home tell box filled in. A shared draft posted twice queues one thought. Return after sign-in was already in place, and I tested it. test/share-target.test.ts passes (5 tests) and tsc, the gate and design-lint are clean.
What changed
- One place for the names.
src/web/share_target.tsnow holds the manifest'sshare_target, the composer field names, the draft-joining (sharedDraft) and the 2000-character cap.standards.ts,studio/feed.tsandstudio/living.tsall import it. - Double submit. A shared draft carries a hidden
oncetoken, a hash of the person and the draft.POST /tellclaims it, so a second post of the same share answers "Already sent." and creates nothing. Both the home tell box and the feed composer carry the token. - Tests.
- The manifest declares exactly the shared names.
- Title, text and url are joined, and markup in them is escaped.
- A 6,000-character share is cut to 2000.
- A signed-out sharer gets a sign-in link whose next is the full shared URL; after sign-in they land on that URL, and //, https:// and \ return paths are refused.
- Posting the shared form twice adds exactly one thought.
What I left out
- The dedupe lives in process memory for 10 minutes, so a restart forgets it. A table would need a migration, which was more than this change needed.
- Only an identical share is deduped. Plain composer posts are not.
- I didn't test a 50 KB query string, because the request fails before the page renders. The cap itself is tested at 6,000 characters.
Checks I couldn't complete
- One test in
test/composer-draft.test.tsneeds a real browser, and this sandbox has none. It failed here. scripts/fast.tscalled the diff too broad, so I ran the eight test files that cover the composer, tell box, manifest and home. Everything else passed or was skipped.- The first push was blocked by a hook that wants a "Try:" line in a commit message, so I added an empty commit carrying one.
Try it: signed in, open /?title=Hi&text=make+it+blue&url=https://example.com, post, and reload the same link and post again. Only one thought is kept.
The share path works and is tidy, but the double-submit guard can lose a person's tell, and the contract is not fully in one place. Fix first: claim the token only after the tell is created (actions.ts). Also: (1) the guard claims before validation, so a failed post burns the token and the retry answers 'Already sent.' with nothing queued; (2) a missing or malformed token skips the guard (claimShare returns true), so dropping the field defeats it; (3) COMPOSER_FIELDS.back is declared but feed.ts and actions still hard-code 'back', so box 4 is not met; (4) the 'export { sharedDraft }' shim in feed.ts is a leftover, so import from share_target.ts directly; (5) no docs/features/ how-to for sharing to the forge from a phone (not README); (6) the terminal door is not addressed: say in the pull why a share needs none, or add the screen; (7) the test only exercises the home tell box, so the feed composer's textarea (escaping, once token) and the failed-post-then-retry case are untested; (8) claimShare's eviction loop deletes everything once full, so simplify it.
All eight points are addressed and pushed. tsc, gate.ts, design-lint and 55 tests across 8 files covering /tell and the composers pass. I did not run the full suite.
- Token claim (actions.ts): I did not literally claim after the tell is created. The token is claimed before the tell and given back unless a tell or thought was actually made. I kept it up front because two simultaneous submits would otherwise both create a tell. The effect you asked for is the same:
- A failed post (empty, too long, unknown app, thrown error) releases the token, so the retry is queued. - A duplicate answers "Already sent." and queues nothing. - This also covers point (1).
- Missing or malformed token (2): No
oncefield is an ordinary tell. Aoncethat is not 24 hex characters is refused with "That share could not be sent." and nothing is queued.claimSharenow returns"fresh" | "seen" | "bad". backfield (3):feed.tsandcomposerSubmitboth useCOMPOSER_FIELDS.back, and the text field goes throughCOMPOSER_FIELDStoo.backToinform.tsstill reads"back"for the other forms, socomposerSubmitchecksisLocalPathdirectly instead.- Re-export shim (4): I deleted
export { sharedDraft }fromfeed.tsand updated the stale comment instandards.ts. - How-to (5):
docs/features/share-to-the-forge.md. The README is untouched. - Terminal door (6): A share needs none. The share sheet is a phone and browser feature that arrives as
GET /?text=…, so there is no terminal path to reach it from. The terminal's tell, if it has one, is unchanged. - Tests (7):
test/share-target.test.tsnow covers these cases:
- The /feed composer's textarea is escaped, carries the once token and uses the back name.
- A failed post followed by a retry queues one thought, and a second send of the same retry is refused.
- A malformed token is refused, and no token is an ordinary tell.
- Eviction and release, tested directly.
- Eviction (8):
claimSharedrops the oldest entries first, one at a time, until the map is under the limit.
Try it: signed in, open /?text=hello&url=https://example.com and post twice; one thought is kept.
🤖 Generated with Claude Code
The failing check was caused by main, not by the share-sheet change. I've pushed a fix to the branch and have not seen the forge's check run on it yet.
What was wrong: commit 54dacba00 on main retired src/kernel/why_now.ts and left it in place "until the next sweep". Nothing imports it any more, so the test "every module under src/ is imported by another, or allowed" failed for any branch built on that main. Before I merged main in, the test passed locally on this branch alone.
What I changed: I merged current main into the branch and added src/kernel/why_now.ts to scripts/unreached-allow.json. That list is the repository's own way to say "kept on purpose". The entry should come out when the module is deleted.
What I ran: after the change, test/unreached.test.ts and test/share-target.test.ts pass (13 tests) and bun x tsc --noEmit is clean. I did not run the full suite or bun scripts/gate.ts, because the forge runs the full check on push.
What I chose not to do: I did not delete why_now.ts or its test. That module belongs to #10317 and the sweep that follows it, not to the share-sheet work.
How to try it: open /?text=hello&url=https://example.com signed in, then post twice. One thought is kept.
Since 006749f6f1ee the head only merges main and adds src/kernel/why_now.ts to scripts/unreached-allow.json. That fixes the red check main caused, and the entry should leave the list when the module is deleted. Earlier asks: the token is released on failure and refused when malformed, the field names and the shim removal are done, the how-to is docs/features/share-to-the-forge.md, README is untouched, tests cover the feed composer, retry, bad token and eviction, and the oldest entries are evicted first. I accept the answer on the terminal door: a share sheet arrives as a browser GET, so it has no terminal path. No new component, no new accent, and everything works without JavaScript. Two things I would do next. Non-blocking: persist the dedupe in the db so a restart does not forget it. Non-blocking: delete why_now.ts and its allow-list entry in the sweep after #10317.
A phone share now opens the home tell box filled in. A shared draft posted twice queues one thought. Return after sign-in was already in place, and I tested it.
test/share-target.test.tspasses (5 tests) andtsc, the gate and design-lint are clean.What changed
src/web/share_target.tsnow holds the manifest'sshare_target, the composer field names, the draft-joining (sharedDraft) and the 2000-character cap.standards.ts,studio/feed.tsandstudio/living.tsall import it.oncetoken, a hash of the person and the draft.POST /tellclaims it, so a second post of the same share answers "Already sent." and creates nothing. Both the home tell box and the feed composer carry the token.- The manifest declares exactly the shared names. - Title, text and url are joined, and markup in them is escaped. - A 6,000-character share is cut to 2000. - A signed-out sharer gets a sign-in link whose
nextis the full shared URL; after sign-in they land on that URL, and//,https://and\return paths are refused. - Posting the shared form twice adds exactly one thought.What I left out
Checks I couldn't complete
test/composer-draft.test.tsneeds a real browser, and this sandbox has none. It failed here.scripts/fast.tscalled the diff too broad, so I ran the eight test files that cover the composer, tell box, manifest and home. Everything else passed or was skipped.Try it: signed in, open
/?title=Hi&text=make+it+blue&url=https://example.com, post, and reload the same link and post again. Only one thought is kept.Closes #10390