Conversation
Contacts are the active workers; call walks over and opens their terminal, SMS sends a worker.prompt, all in an iFruit-styled modal. No new wire protocol in MVP. Docs-only change.
J (or the Smartphone HUD entry) opens a phone whose contacts are the workers on the floor. A call rings twice, then puts you at their desk with the terminal open; an SMS sends a worker.prompt and is kept in a per-floor thread in the browser. No new wire protocol: both ride existing messages. New self-contained features/smartphone/ module plus one-line registry joins, J help row, synthesized ring/swoosh/blip, unit tests and docs.
iptoux
left a comment
There was a problem hiding this comment.
Review: PR #299 — GTA-style smartphone for the player character
Clean, well-scoped PR: self-contained features/smartphone/ module, no new wire protocol, pure helpers with unit tests. No blockers. Verdict: approve once items 1–5 are addressed; items 6–13 are nits that can land as follow-ups.
Findings
- [Security]
src/client/features/smartphone/ui.ts:100,146,217,273—w.coloris interpolated intostyle="background:..."viasetAttributeat four call sites. Peer colors areCOLOR_RE-checked server-side today, but nothing at these call sites constrains the value, so a;in a color becomes arbitrary CSS declarations (e.g.background-image:url(...)phone-home). Fix: validate against/^#[0-9a-fA-F]{6}$/with a fallback, or assignel.style.backgroundColor. - [Security]
src/shared/smartphone.ts:57(written fromsrc/client/features/smartphone/ui.ts:262) — SMS bodies (raw agent prompts, which routinely contain code/secrets) are persisted in plaintextlocalStorage, surviving session/logout and readable by any JS on the origin. Fix: don't persist message text (keep only recents metadata), or make thread persistence opt-in with a visible clear control. - [Correctness]
src/client/features/smartphone/ui.ts:193— Lost-worker guard misseslost && !worktree: a worker whose multi-repo workspace is gone haslostset with noworktree, falls through, logs a call recent, teleports (closing the phone), thenopenWorkerTerminal→fixLostWorktreesilently no-ops (if (!w.lost || !w.worktree) return,features/workers/actions.ts:222). The user ends at the desk with no terminal and no explanation. Fix: branch onnow.lostalone (the pattern infeatures/waiting/index.ts:117) and toast when there is nothing to rebuild. - [Correctness]
src/client/features/smartphone/ui.ts:198-201— Failed calls are logged as placed:logRecent(kind: 'call')runs beforegoToWorker(), so when it returns false (between floors, desk gone) a phantom "outgoing call" stays in Recents. Fix: move thelogRecent/emitafter the successfulgoToWorkercheck. - [Correctness, Security]
src/shared/smartphone.ts:49(crashes atsrc/client/features/smartphone/ui.ts:274) —loadThreadschecksArray.isArray(v)but not entry shape, so a crafted or stalelocalStorageentry (null, strings) throws onm.dir/m.textand bricks the phone until storage is cleared. Fix: keep only well-formed{dir: 'out'|'note', text: string, at: number}entries on load (and cap text length). - [Correctness]
src/client/features/smartphone/ui.ts:254-258— SMS submit silently discards text: the thread view never redraws on store updates (by design,onStoreskipsthread), so if the worker falls asleep while the thread is open the hint/composer go stale and the guard returns with no feedback. Fix: toast on refusal, and/or live-update the hint/disabled state without a full redraw. - [Security]
src/client/features/smartphone/ui.ts:249— SMS<input>has nomaxlengthwhile the server truncates at 20000 (src/server/ws/handlers/workers.ts:119); oversized pastes silently truncate mid-instruction and burn tokens. Fix: addmaxlengthmatching the server cap. - [Performance]
src/shared/smartphone.ts:57(called fromsrc/client/features/smartphone/ui.ts:262) — every SMS serializes the entire thread map tolocalStorage, and keys (floor/worker) accumulate forever: per-thread caps exist but nothing bounds the thread count, so stringify + write grows without bound. Fix: persist only the touched key, or prune keys for floors/workers that are gone / cap total keys. - [Correctness]
src/client/features/smartphone/ui.ts:281-286— Back/tabs duringcallingsilently cancel the pending connect: any view switch runsdraw()→clearTimers(), killing theCONNECT_MStimeout after the ring already played, with no feedback. Fix: route Back duringcallingthrough the End-button path (back toactions), and/or ignore tab switches while calling. - [Performance]
src/client/features/hud/index.ts:55— the badgecountrunswaitingInOrder(store.workers.values()).lengthon every HUD refresh, allocating and sorting the list just for a.length. Fix: reuse the existingwaitingNow()helper or count without sorting. - [Performance]
src/client/features/smartphone/ui.ts:52(viaonStoreat:290) — everyworkers/smartphoneevent re-runsbyUrgency(two sorts, several copies) andreplaceChildrens the whole contact list, even when only recents changed or the visible tab is unaffected. Fix: redraw only the visible list, or extend the existingthread/callingskip-guard to events that can't affect the current view. - [Correctness]
src/client/features/smartphone/ui.ts:124-135— Recents go stale: names are snapshotted at log time (a renamed worker shows its old name), and rows for sent-home workers are never pruned (tap → "went home" toast, row stays until 20-cap eviction). Fix: resolve display names fromstore.workersat render and drop rows whose worker is gone. - [Simplicity]
src/client/features/smartphone/sound.ts:9— theunlock/ctx/resume/countboilerplate is triplicated verbatim acrossphoneRing,smsSwooshanddialBlip. Fix: extract a smallwithCtx(name, fn)helper and pass the three recipes through it.
- Validate worker dot colors (plain hex only, fallback otherwise). - Threads are plaintext in this browser like terminal scrollback; Recents gets a visible Clear message history control. - Calls branch on lost alone, toast when nothing can be rebuilt; lost workers get a Fix it view instead of a composer. - Log call recents only once the call is really placed. - loadThreads keeps only well-formed lines, capped in length. - SMS refusal toasts; thread hint/composer follow the worker live. - Composer maxlength matches the server's 20000 truncation. - Persisted threads capped (count + per-thread), quietest evicted. - Back/tabs during a call hang up to actions, never cancel silently. - HUD badge reuses waitingNow(); store events redraw only the visible list; recents resolve names live and prune gone workers. - Extract the sound-context boilerplate into ready().
|
All 13 findings addressed in 880e550 (verified: typecheck, 614 tests, build, fresh screenshots of contacts/thread/call/recents): Blockers
Nits Two environment notes (no product impact): the lab screenshot page emulates a speech-less browser because headless Chromium crashes in |
iptoux
left a comment
There was a problem hiding this comment.
Review: PR #299 — GTA-style smartphone
Verdict: request changes. Fix the __proto__ thread-key write, the lost-worker call/SMS paths (phantom recents, texting/fix-flow races), and the thread-persistence shape (top-level guard + in-memory cap + quota-sized caps) before merge. The rest is follow-up cleanup.
- [Security]
src/shared/smartphone.ts:63—loadThreads()assigns parsed-JSON keys straight into a plain object, so a craftedlocalStorageentry with"__proto__"mutates the prototype instead of creating a thread. Skip__proto__/constructor/prototypekeys (or build withObject.create(null)); same guard for theObject.fromEntrieseviction path insaveThreads(). - [Correctness]
src/client/features/smartphone/ui.ts:246—startCalllogs the call to recents beforeopenWorkerTerminalruns, so a worker going lost/removed in between leaves a phantom call despite the "no phantoms" comment. Re-checklost/existence aftergoToWorkerand only thenlogRecent, or log after a successful terminal open. - [Correctness]
src/client/features/smartphone/ui.ts:322— SMS submit checks gone/asleep but notlost, so a worktree deleted between render and send still firesworker.promptat a worker that can't act. Checknow.lostand route to the fix-worktree flow instead of sending. - [Correctness]
src/client/features/smartphone/ui.ts:365—liveThreadonly follows asleep/wake flips; a worker turninglostwhile its thread is open keeps the composer instead of switching to the "Fix it" UI. Detectw.lostinliveThreadanddraw()the lost branch. - [Correctness]
src/client/features/smartphone/ui.ts:201— "Go to desk" callsgoToWorkerunconditionally, walking to a lost worker's desk instead of the fix flow Call/SMS/promptAtDeskuse. Checkw.lostand callfixLostWorktree(or block with toast). - [Performance & Simplicity]
src/shared/smartphone.ts:57-73+src/client/features/smartphone/ui.ts:330-335— every SMSsaveThreads()stringifies the whole map (up toMAX_KEYS(50)×MAX_THREAD(100)×MAX_SMS_TEXT(20000), far over the ~5MB quota) synchronously on the send path. Lower the caps and/or persist per-thread/debounced instead of full-map-per-send. - [Correctness]
src/shared/smartphone.ts:53—loadThreadsrunsObject.entries(parsed)without checking parsed is a plain object; stored[]/"hi"/[[valid]]yields bogus"0"/"1"thread keys. Early-return{}unless parsed is a non-null, non-array object. - [Correctness]
src/shared/smartphone.ts:72—saveThreadsevicts toMAX_KEYSonly for storage; the in-memorystore.smartphone.threadskeeps growing since the evicted copy is never assigned back. Return the kept map and assign it, or cap in-memory threads the same way. - [Correctness]
src/client/features/smartphone/ui.ts:134—renderRecentsmutatesst.recents = aliveduring render with nostore.emit, anddrawTabs(ui.ts:59) already read the pre-filter length, so the tab count is stale one draw and other listeners miss the drop. Emit after filtering, or filter outside render. - [Correctness]
src/client/features/smartphone/ui.ts:158— "Clear message history" clearsthreadsbut leavessmsrecents pointing at now-empty threads. Also dropsmsrecents (or all recents) on clear, or rename the button to "clear threads". - [Performance & Simplicity]
src/client/features/smartphone/ui.ts:377-387— everyworkersevent triggers a fulldraw()→replaceChildren()rebuild of the contacts/actions list, and worker activity ticks fire often, so the open phone churns DOM constantly. Throttle/coalesce (rAF) and skip redraw when membership + sort order + visible fields are unchanged. - [Performance & Simplicity]
src/client/features/smartphone/ui.ts:52-80—draw()sorts twice per paint:drawTabs()runsbyUrgency(store.workers.values())just forcontacts.length, thendrawBody()runs it again. Compute once indraw()and pass down; usestore.workers.sizefor the tab count. - [Performance & Simplicity]
src/client/features/hud/index.ts:48,55,114-122—waitingNow()(=waitingInOrder: copy + filter + sort) is re-evaluated ~6× per HUD refresh (count,icon,shown,status,chip,on,tone). Compute once per refresh and reuse. - [Performance & Simplicity]
src/client/features/smartphone/ui.ts:32-35,259,350— onetimers[]array mixes the Dialing→Ringing phase timer with the 2600ms connect timer, so any redraw silently re-times the phase. Track the connect handle separately, and track (or drop) thesetTimeout(() => input.focus(), 30)so it can't fire after close/view change. - [Performance & Simplicity]
src/client/features/smartphone/ui.ts:316—renderThreadbuilds a freshdictateField(input)(→dictation()+ listeners) on every render, including right after each send, withoutdrop()on the old one. Create once per thread view and dispose on close/redraw. - [Performance & Simplicity]
src/shared/smartphone.ts:64-69— eviction recomputeslatest(k)(a fullreduceover up to 100 msgs) inside the sort comparator → O(K²·T). Precomputelatestonce into a Map before sorting. - [Performance & Simplicity]
src/client/features/smartphone/ui.ts:29-400—openSmartphoneis a ~370-line closure holding 5 views, timers,livepatching, and both store subscriptions; the plan (§8) said to split past ~350 lines. Split intocontacts.ts/call.ts/sms.ts(or extract tab/body/draw helpers). - [Correctness]
src/client/features/smartphone/logic.ts:42—statusNotedefault treats any unknown future status as asleep ("wake it before texting"). Default to a neutral delivered/unknown line instead.
- Split the phone shell into ui.ts + contacts.ts + call.ts + sms.ts. - Per-thread localStorage keys: one small write per text; load validates shape, skips __proto__/constructor/prototype, drops malformed and over-cap keys (from storage too); in-memory map capped the same way. - Calls re-check existence/lost after the walk-over and log only connected calls; SMS submit and Go-to-desk route lost workers to the fix flow; live thread switches to Fix-it UI when a worker turns lost. - pruneRecents runs at the top of every draw (no render-time mutation); clearing also drops sms recents. - Workers events coalesced per frame and skipped when nothing visible changed; draw() sorts once; HUD waiting list memoized per workers change. - Connect timer tracked separately; focus timer tracked; mic created once per thread view and dropped on the way out. - statusNote defaults to a neutral line for unknown statuses.
|
Second round addressed in e8143d7 (verified: typecheck, 616 tests, build, fresh screenshots of contacts/thread/call/recents): Storage ()
Lost races + phantoms (, , )
Churn (, )
|
|
Correction to the headings above — the file paths were eaten by shell interpolation. They should read: Storage (src/shared/smartphone.ts) Content of the comment is otherwise as intended. |
|
Verified Checked each finding from the request-changes review against the new code:
One non-blocking nit I noticed while verifying (not in the review): |
The badge was gated on w.pr && w.activity while contactSub showed PR #n with no activity, so the signal moved places. Now contactSub never mentions PRs and both contact rows and the actions view always show the badge when w.pr is set.
|
Badge nit fixed in 953a5a3: On merge: leaving that to you — per repo rules I only merge webdevcody's PRs (others only when linked, after a security review), and this one has neither, so I won't press the button myself. |
iptoux
left a comment
There was a problem hiding this comment.
Review: PR #299 — GTA-style smartphone for the player character
Summary and verdict
Solid, well-scoped PR: self-contained features/smartphone/ module, no new wire protocol,
pure helpers with unit tests, storage handling done carefully. Not green yet — changes
requested. The panel found one real UI bug (phone renders a blank body when a worker
vanishes mid-view), a stale-contacts skip, and a handful of performance nits plus minor
correctness polish. [Security] reports no findings.
Findings
- [Correctness]
src/client/features/smartphone/ui.ts:143(drawBody) — blank phone body
when a worker vanishes mid-view.renderActions/renderCalling/renderThreadhandle a
missing worker viaphone.go({t:'contacts'})(which synchronously re-draws and paints the
fallback) and then return[]; the outerdrawBodythen runsbody.replaceChildren()with
nothing and wipes the just-painted fallback. Reachable: phone open on a worker's actions or
thread, worker sent home →workersevent →draw()→ empty body (recovers on tab tap).
Fix: snapshotviewbefore rendering indrawBodyand skipreplaceChildrenwhen the
renderer redirected. - [Correctness]
src/client/features/smartphone/ui.ts:110(sig) — stale contacts when a
worker turnslost.sigcovers id/name/color/status/activity/pr but notlost(nor
kind), soonWorkersskips the redraw and the row keeps its old sub-line; the 🌿 fix-flow
only appears on the next tap/draw. Fix: includelost(andkind) insig. - [Performance]
src/client/features/smartphone/sms.ts:73(renderThread) — renders all 50
capped messages at full 20k chars each into the DOM at once; a long thread builds a huge node
tree on every open and janks scroll. Render a window (e.g. last 20 with a "show earlier"
expander) or truncate long bubbles with expand-on-tap. - [Correctness, Performance]
src/client/features/smartphone/sound.ts:20(phoneRing) —
ringback can't be silenced. Both rings' oscillators are scheduled fire-and-forget with no
stop handle, so End/hang-up/close mid-dial still plays out (~1.5s) with no call attached and
holds osc+gain nodes for nothing. Fix: return a stop handle (or schedule ring 2 via the
connect/view timer) and stop it inhangUp/clearConnect/onClose. - [Performance]
src/client/features/smartphone/ui.ts:75(sig()) — the redraw skip-check
joins the fullactivitytext of every worker, rebuilding a large string on frequent
activity ticks just to decide not to redraw. Sig over cheap stable fields only (id/status/
color/pr number + activity length or a truncated prefix). - [Correctness]
src/client/features/smartphone/sms.ts:124(liveThread) — stale thread
header on rename. Live updates refresh the hint/placeholder/disabled state but not the
💬 <name>header, so a renamed worker shows its old name until the view is re-entered. Fix:
update the header node inliveThreadtoo (or store a ref to it inThreadLive). - [Performance]
src/client/features/smartphone/contacts.ts:87(renderRecentstitle) —
new Date(r.at).toLocaleString()runs per row on every recents draw;toLocaleStringis
slow for a tooltip nobody may read. Compute it lazily on hover/focus or memoize perat. - [Correctness]
src/client/features/smartphone/call.ts:11(CONNECT_MS = 2600) — ~1s dead
air after the ring. The double-ring envelope ends at ~1.55s ([0, 0.9]+ 0.65s) but connect
fires at 2.6s. Fix: connect at ~1.8s or schedule a third ring to cover the gap. - [Correctness]
src/client/features/smartphone/call.ts:48(connect) — doing-line deviates
from the plan. Plan §3.1/§7 promisesdoing: "📱 on a call with <name>", but the call ends
inopenWorkerTerminal(id), whose doing is hardcoded (ui/terminal.ts:338) to
💻 in <name>'s terminal. Fix: thread a doing override through (or correct the plan/docs). - [Performance]
src/client/features/smartphone/ui.ts:18(Phoneinterface) — ~9 methods
plus a separateThreadLivetype and three view modules for a single 380px modal where each
view has exactly one caller; the indirection costs more reading than it saves. Consider
collapsing to 2–3 primitives (go/after/close) or co-locating the views until a second
caller exists.
…doing line - drawBody snapshots the view and skips replaceChildren when a renderer redirected (worker gone mid-view no longer wipes to a blank body) - sig covers lost and kind, activity as length+prefix (cheap skip check) - thread renders last 20 with a show-earlier expander; sent texts join the live snapshot so expanding stays complete - phoneRing returns a stop handle, silenced on hang-up, failed connects and close; OfficeSound passes it through - live thread header follows renames; recents timestamps memoized - connect at 1.8s, just as the double ring ends (was 2.6s dead air) - call opens the terminal with doing 'on a call with <name>' via a new optional TerminalOptions.doing threaded through openWorkerTerminal - Phone split kept: the previous review required the module split for the size rule; collapsing it now would churn back
|
All 10 findings fixed in
No new unit tests: all ten are DOM/timer/audio paths the node suite can't reach (same reason the chess resign race shipped verified-by-path). The lab page ( |
The main character gets an iFruit-style smartphone (plan in docs/plans/smartphone.md):
Verified: npm run typecheck, npm test (611 pass), npm run build, headless-browser screenshots of contacts / SMS thread / call screen (via src/client/lab/phone.html).