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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

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.ts now holds the manifest's share_target, the composer field names, the draft-joining (sharedDraft) and the 2000-character cap. standards.ts, studio/feed.ts and studio/living.ts all import it.
  • Double submit. A shared draft carries a hidden once token, a hash of the person and the draft. POST /tell claims 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.ts needs a real browser, and this sandbox has none. It failed here.
  • scripts/fast.ts called 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.

Closes #10390

commented

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.ts now holds the manifest's share_target, the composer field names, the draft-joining (sharedDraft) and the 2000-character cap. standards.ts, studio/feed.ts and studio/living.ts all import it.
  • Double submit. A shared draft carries a hidden once token, a hash of the person and the draft. POST /tell claims 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.ts needs a real browser, and this sandbox has none. It failed here.
  • scripts/fast.ts called 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.

Changes requested — before the latest push

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.

commented

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 once field is an ordinary tell. A once that is not 24 hex characters is refused with "That share could not be sent." and nothing is queued. claimShare now returns "fresh" | "seen" | "bad".
  • back field (3): feed.ts and composerSubmit both use COMPOSER_FIELDS.back, and the text field goes through COMPOSER_FIELDS too. backTo in form.ts still reads "back" for the other forms, so composerSubmit checks isLocalPath directly instead.
  • Re-export shim (4): I deleted export { sharedDraft } from feed.ts and updated the stale comment in standards.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.ts now 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): claimShare drops 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

commented

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.

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs