Settings sheet — 電波 / 相手 / 局 / 本体 #29

Closed
opened 2026-07-30 00:03:49 +00:00 by aiko · 3 comments
Owner

What to build

The settings sheet. Replaces #14, whose reality audit is wrong in three places.

- address, pairing state (ADR-0004), and the 自発送信 cron list, which is not cut. The adapter calls list_jobs() / get_job() in-process, and pause_job() / resume_job() is the mute: no dashboard HTTP, no session token, no relay (ADR-0007). Records already carry id, name, state, enabled, schedule_display, next_run_at, last_run_at, last_status, last_error. Read-and-mute only; scheduling stays in Hermes.

Also in 局: the upstream readout is real now, because the adapter socket is live state. See the two-hop issue.

相手 - Live2D model and scale, plus a read-only voice line. No persona row: her personality lives in Hermes' SOUL.md and the client neither shows nor edits it (ADR-0009). A deliberate absence, not a gap.

電波 - VAD thresholds and hands-free, unchanged.

本体 - client version and both reset levels, each stating plainly that the log, her memory and her character live on the server. The client must not offer to delete her.

Acceptance criteria

  • Cron jobs list with real schedules; mute suppresses without editing the schedule
  • A mute survives a restart and is visible from hermes cron list
  • No persona control exists anywhere in the client
  • 声 is a readout, not a picker
  • Reset never implies her memory or character is client-side

Reference

ADR-0004, 0007, 0009. Design: 3b, 3c, 5b, 7a.

## What to build The settings sheet. Replaces #14, whose reality audit is wrong in three places. **局** - address, pairing state (ADR-0004), and the 自発送信 cron list, which is **not** cut. The adapter calls `list_jobs()` / `get_job()` in-process, and `pause_job()` / `resume_job()` is the mute: no dashboard HTTP, no session token, no relay (ADR-0007). Records already carry `id`, `name`, `state`, `enabled`, `schedule_display`, `next_run_at`, `last_run_at`, `last_status`, `last_error`. Read-and-mute only; scheduling stays in Hermes. Also in 局: the upstream readout is real now, because the adapter socket is live state. See the two-hop issue. **相手** - Live2D model and scale, plus a read-only voice line. **No persona row**: her personality lives in Hermes' `SOUL.md` and the client neither shows nor edits it (ADR-0009). A deliberate absence, not a gap. **電波** - VAD thresholds and hands-free, unchanged. **本体** - client version and both reset levels, each stating plainly that the log, her memory and her character live on the server. The client must not offer to delete her. ## Acceptance criteria - [ ] Cron jobs list with real schedules; mute suppresses without editing the schedule - [ ] A mute survives a restart and is visible from `hermes cron list` - [ ] No persona control exists anywhere in the client - [ ] 声 is a readout, not a picker - [ ] Reset never implies her memory or character is client-side ## Reference ADR-0004, 0007, 0009. Design: 3b, 3c, 5b, 7a.
Author
Owner

Branch: issue/denpa-29-settings-sheet (off main). npm test is 559 passed in 40 files (main was 511 in 37). npm run typecheck still reports exactly 585 errors and none of them names a file this branch touched.

What landed

A 調整 sheet with the four sections, reached from a 調整 key on the station bar (44px, aria-expanded) — #12 removed the sidebar that used to carry a way in, and the key is absent unless a caller passes a handler, the same shape #31 used for 画面. Wide screens get the right 600px; a phone gets a bottom sheet with a grab handle. It draws over the stage without unmounting it, so she stays visible.

  • 電波 — しきい値 as one meter with the 発話 and 無音 lines drawn on it at their real heights, operated by two native range inputs clamped against each other so 無音 ≥ 発話 is unrepresentable. 常時受信 (the existing mic toggle) and 走査線, which finally consumes the onScanlineToggle #12 left with no control. No per-setting ASR or TTS: whole-config switching is the only mechanism there is.
  • 相手 — the installed character configs as cards (switch-config, the real model mechanism), and 大きさ bound to pal-scale's per-monitor store, so the slider and the 拡縮 grip are one value. No 人格 row anywhere (ADR-0009), asserted across all four tabs as an absent row and an absent control label.
  • — the address with 確認 (committed on the key, not per keystroke, or it would reconnect per character), 端末承認 stating that there is no key field and approval happens in Hermes (ADR-0004), the upstream hop read from two-hop's model (#27), and the 経路 note.
  • 本体 — client version (pinned to package.json by a test), repo and Cubism license links, the Android overlay permission row driving openOverlaySettings() — the OS screen that already exists — and both reset levels, each arming on the first press and committing on the second. Reset removes only keys this client wrote and never asks the server to forget anything. No log export.

Deliberately left out, with the reason

  • The 自発送信 cron list is not built, so acceptance criteria 1 and 2 are unmet. ADR-0007 is right that list_jobs()/pause_job() are in-process and the records carry every field 局 draws — but nothing has been written. hermes_plugin/denpa/adapter.py never calls them (only a docstring at :17 and the tick hook at :638), protocol.py:45-62 defines no frame for a job list or a mute, and the go-between's client handler map (websocket_handler.py:66-83) has no case for either. There is no schedule to render and no mute to send. This wants a go-between issue: two frames, the adapter calling cron.jobs, two client message types.
  • 声 has no row either. Nothing the go-between sends names a voice or an engine — set-model-and-conf carries model_info, conf_name, conf_uid, client_uid and nothing else (websocket_handler.py:171-181). "声 is a readout, not a picker" is met by there being neither, and a readout of nothing would be worse.
  • Pairing state is a sentence, not a status. Nothing in the go-between implements pairing, and the client sends no device_id (routes.py:37 reads one from the query string; the client never supplies it), so there is no approved/pending to draw.
  • 画面を見せる stayed on the station bar rather than moving into 電波 per § 5d: the ask card draws over the stage and the sheet covers the stage, so a row here would either open a card the sheet hides or need a second copy of it.

Assumptions worth checking

  • The two reset levels split as tuning vs tuning plus the address. § 7a lists the address in level one, but ADR-0004 removed the token from that list and clearing the address at level one would leave 最初からやる with nothing left to do.
  • denpa#35 will have to re-point one prop. onThresholdsChange writes VADContext.updateSettings, which is the MicVAD store; silero-in-Rust owns those numbers instead. Nothing else in the sheet touches the audio path, and the wiring test pins that it calls no browser media API.

On test shape

The vitest environment is node — no DOM, no testing-library — so events cannot be fired. Rather than assert that handlers are attached (the #19 defect), every control is built from a natively focusable element and the tests prove it: a test walks every data-denpa-hit on all four tabs and fails if any is not a button/input/a, the thresholds and the scale assert type="range" (arrows and Home/End for free), the toggles assert type="checkbox", nothing carries tabindex="-1", and a source check fails if any control ever activates on a pointer event alone. The interaction logic — the threshold clamps, the reset key computation, the two-press arm — is pure and tested directly.

Eight entries appended to the questions ledger.

Branch: `issue/denpa-29-settings-sheet` (off `main`). `npm test` is **559 passed in 40 files** (main was 511 in 37). `npm run typecheck` still reports exactly 585 errors and none of them names a file this branch touched. ## What landed A 調整 sheet with the four sections, reached from a `調整` key on the station bar (44px, `aria-expanded`) — #12 removed the sidebar that used to carry a way in, and the key is absent unless a caller passes a handler, the same shape #31 used for `画面`. Wide screens get the right 600px; a phone gets a bottom sheet with a grab handle. It draws over the stage without unmounting it, so she stays visible. - **電波** — しきい値 as one meter with the 発話 and 無音 lines drawn on it at their real heights, operated by two native range inputs clamped against each other so 無音 ≥ 発話 is unrepresentable. 常時受信 (the existing mic toggle) and 走査線, which finally consumes the `onScanlineToggle` `#12` left with no control. No per-setting ASR or TTS: whole-config switching is the only mechanism there is. - **相手** — the installed character configs as cards (`switch-config`, the real model mechanism), and 大きさ bound to `pal-scale`'s per-monitor store, so the slider and the 拡縮 grip are one value. **No 人格 row anywhere** (ADR-0009), asserted across all four tabs as an absent row *and* an absent control label. - **局** — the address with 確認 (committed on the key, not per keystroke, or it would reconnect per character), 端末承認 stating that there is no key field and approval happens in Hermes (ADR-0004), the upstream hop read from `two-hop`'s model (#27), and the 経路 note. - **本体** — client version (pinned to `package.json` by a test), repo and Cubism license links, the Android overlay permission row driving `openOverlaySettings()` — the OS screen that already exists — and both reset levels, each arming on the first press and committing on the second. Reset removes only keys this client wrote and never asks the server to forget anything. No log export. ## Deliberately left out, with the reason - **The 自発送信 cron list is not built, so acceptance criteria 1 and 2 are unmet.** ADR-0007 is right that `list_jobs()`/`pause_job()` are in-process and the records carry every field 局 draws — but nothing has been written. `hermes_plugin/denpa/adapter.py` never calls them (only a docstring at :17 and the tick hook at :638), `protocol.py:45-62` defines no frame for a job list or a mute, and the go-between's client handler map (`websocket_handler.py:66-83`) has no case for either. There is no schedule to render and no mute to send. This wants a go-between issue: two frames, the adapter calling `cron.jobs`, two client message types. - **声 has no row either.** Nothing the go-between sends names a voice or an engine — `set-model-and-conf` carries `model_info`, `conf_name`, `conf_uid`, `client_uid` and nothing else (`websocket_handler.py:171-181`). "声 is a readout, not a picker" is met by there being neither, and a readout of nothing would be worse. - **Pairing state is a sentence, not a status.** Nothing in the go-between implements pairing, and the client sends no `device_id` (`routes.py:37` reads one from the query string; the client never supplies it), so there is no approved/pending to draw. - **画面を見せる stayed on the station bar** rather than moving into 電波 per § 5d: the ask card draws over the stage and the sheet covers the stage, so a row here would either open a card the sheet hides or need a second copy of it. ## Assumptions worth checking - The two reset levels split as *tuning* vs *tuning plus the address*. § 7a lists the address in level one, but ADR-0004 removed the token from that list and clearing the address at level one would leave 最初からやる with nothing left to do. - **denpa#35 will have to re-point one prop.** `onThresholdsChange` writes `VADContext.updateSettings`, which is the `MicVAD` store; silero-in-Rust owns those numbers instead. Nothing else in the sheet touches the audio path, and the wiring test pins that it calls no browser media API. ## On test shape The vitest environment is `node` — no DOM, no testing-library — so events cannot be fired. Rather than assert that handlers are attached (the #19 defect), every control is built from a natively focusable element and the tests prove it: a test walks every `data-denpa-hit` on all four tabs and fails if any is not a `button`/`input`/`a`, the thresholds and the scale assert `type="range"` (arrows and Home/End for free), the toggles assert `type="checkbox"`, nothing carries `tabindex="-1"`, and a source check fails if any control ever activates on a pointer event alone. The interaction *logic* — the threshold clamps, the reset key computation, the two-press arm — is pure and tested directly. Eight entries appended to the questions ledger.
Author
Owner

Branch issue/denpa-29-settings-sheet, pushed. Partially delivered — acceptance criteria 1 and 2 are UNMET. Read the wire section before deciding to merge.

What was built

調整, a four-tab settings sheet mounted inside the receiver stage, plus its wiring:

  • src/renderer/src/components/receiver/settings-spec.ts — all copy, key lists, reset levels, and the pure interaction logic (threshold clamps, the ThresholdGesture reducer, the reset key computation, the two-press arm).
  • src/renderer/src/components/receiver/settings-sheet.tsx — the sheet itself: 局 (address + upstream row + 端末承認 prose), 相手 (姿 cards via the existing useSwitchCharacter), 調整 (しきい値, 大きさ), 本体 (version, overlay permission, two reset levels).
  • src/renderer/src/hooks/receiver/use-settings.ts — address/threshold/scale/permission/reset state.
  • Additive changes only to use-receiver-chrome.ts, station-bar.tsx, receiver-chrome.tsx: the 調整 key is an optional prop mirroring #31's onShareKey, so a caller that passes nothing sees no change.

Tests: 575 passed in 40 files (main baseline 511 in 37). Typecheck is 585 errors, identical to the recorded main count, and grepping the output for every touched path returns nothing — no new errors. Lint remains broken on main's missing eslint-config-airbnb; untouched.

 Test Files  40 passed (40)
      Tests  575 passed (575)
   Duration  8.30s

One environment fix was needed to get there: the shared node_modules was missing @chakra-ui/react, @emotion/react and both @chatscope packages, so every ChakraProvider test failed to collect. Restored from the lock with npm install --no-save; package.json and package-lock.json are unmodified.

What the tests cover

The pure logic directly — threshold clamping (無音 >= 発話 is unrepresentable), the gesture reducer that decides when a threshold commits, which storage keys each reset level removes, the two-press arming. Plus structural pins: every data-denpa-hit across all four tabs is a natively focusable element (button/input/a/select/textarea) or the test fails; thresholds and 大きさ assert type="range"; toggles assert type="checkbox"; nothing carries tabindex="-1"; a source check fails if the component ever gains onPointerDown/onMouseDown/onTouchStart. A test pins that CLIENT_VERSION matches package.json. A test pins that the sheet, its spec and its wiring call no browser media API (ADR-0002). A test pins that no denpa.* frame name from protocol.py appears in anything this branch added. A test pins the absence of log export.

Wire claims, with citations

  • The client-facing handler map is interrupt-signal, mic-audio-data, mic-audio-end, raw-audio-data, text-input, the three *-response answers, screen-capture, fetch-configs, switch-config, fetch-backgrounds, request-init-config, heartbeat — and nothing for a cron job list or a mute. denpa/src/open_llm_vtuber/websocket_handler.py:66-83
  • Full set of message types the go-between ever sends a client: audio, backend-synth-complete, background-files, config-files, config-switched, control, error, force-new-message, full-text, heartbeat, heartbeat-ack, image, live2d-models/info, notice, request-init-config, set-model-and-conf, text-input, typing, user-input-transcription, plus the three *-request asks. No voice, no TTS engine, no cron, no pairing state. (grep -rn '"type": "' denpa/src/)
  • ASK_KINDS is exactly three: approval, clarify, confirm, each with its *-request/*-response/*_id triple. denpa/src/open_llm_vtuber/adapter_handler.py:59-63. Not consumed here; verified because the prompt asserted it.
  • upstream-status carries a real boolean attached, pushed on connect and on every attach/detach. websocket_handler.py:39 (type constant), :134-148 (upstream_report / broadcast_upstream_status), :167-169 (on connect). 局's upstream row reads this through services/two-hop.ts.
  • The model list is whole-config switching and nothing finer: fetch-configs{type: 'config-files', configs} (websocket_handler.py:479-487), switch-config with file (:489-496), answered by set-model-and-conf carrying model_info, conf_name, conf_uid, client_uid and nothing else (:171-181).
  • /client-ws reads an optional device_id from the query string (denpa/src/open_llm_vtuber/routes.py:37) and the client never supplies one. No pairing enforcement exists anywhere in the go-between — grep -rn 'pairing' denpa/src/ is empty. So 局's 端末承認 row is prose, not state. Note the prompt's flat "the client never sends a device_id" glosses over the route accepting one.
  • Hop one's frame vocabulary is denpa.speak|notice|transcription|typing|image|approval|clarify|slash-confirm|text|voice|photo|interrupt|callback|turn-complete, denpa/hermes_plugin/denpa/protocol.py:45-62. No job list, no mute.
  • list_jobs() / pause_job() / resume_job() are called by nothing in the go-between repo. grep -rn 'list_jobs|pause_job|resume_job|cron' --include=*.py denpa/src denpa/hermes_plugin returns only hermes_plugin/denpa/adapter.py:17 (a docstring), :638 and :641 (the cron-tick hook), and :699 (an env var name).

The prompt was wrong about one thing, and it mattered

The issue body and the design both say the 自発送信 cron list "is not cut" and enumerate the record fields, which reads as though it were available. It is not. Per the greps above: nothing produces a job list, no frame carries one or a mute, and the client handler map has no case for either. The ADR describes a decision that has not been implemented in the go-between.

So the list was left out, and acceptance criteria 1 (cron list with real schedules) and 2 (mute surviving restart) are unmet. Building it would have been shipping a schedule UI with no producer. It needs a go-between issue for a job-list/mute message pair first, and #29 should be recorded as partially delivered rather than closed.

Also deliberately left out

  • 声 readout — no message anywhere names a voice or an engine. The issue's "声 is a readout, not a picker" holds by there being neither, per the design's rule that a deferred thing is omitted entirely rather than greyed out.
  • A live pairing state in 局 — ADR-0004 is not implemented in the go-between, so 端末承認 explains where approval happens (in Hermes) rather than reporting a status. No key field.
  • The 画面を見せる row that design § 5d puts in 電波 — it stayed on the station bar where #31 put it. The ask card draws over the stage and the sheet covers the stage, so a row here would either open a card the sheet hides or need a second copy of the card. Ledgered with the ~10-line fix.
  • Deleting the orphaned upstream settings tree (components/sidebar/setting/*, hooks/sidebar/setting/*, including agent.tsx's allowProactiveSpeak/idleSecondsToSpeak, which ADR-0007 puts upstream). Nothing in 調整 renders any of it — which is what the issue asked — but the files are still on disk, unmounted since #12, and use-general-settings.ts still exports the two image-compression keys use-media-capture.tsx imports. Deletion is its own sweep and touching those files invites a sibling conflict. Ledgered.
  • 中継局 and 上流 version rows in 本体 (design § 7a cuts them; nothing reports either), and log export (explicitly cut, absence pinned by a test).

What could not be verified

Nothing here is visually confirmed. I cannot see rendered output or drive a live turn. The sheet's 600px placement, the bottom-sheet geometry at 370px, whether the threshold lines land where the meter bars suggest, and the final z-order against #31's screen-share indicator are all reasoned from code, not seen.

More importantly: the vitest environment is node, and neither jsdom nor testing-library is installed, so no test in this repo can fire an event. I could not click a key, press a key, or drag a slider. What I did instead, given the #19 warning, is the structural and pure-logic coverage listed above — every control is a natively focusable element, ranges give arrows and Home/End by construction, and the interaction logic that isn't the platform's is pure and tested directly. That is stronger than "a handler is attached", but it is not a keystroke reaching a control and I am not claiming it is.

Also unverified: onReset calls window.location.reload(), which no test exercises; the Android overlay-permission row, which needs a real Android build to appear at all; and the address commit path, which needs a running go-between.

Assumptions

  • The two reset levels split as tuning vs tuning-plus-address. § 7a puts the address in level one, but ADR-0004 removed the token from that list, and clearing the address at level one would leave 最初からやる with nothing to do. Level two is "back to 待受" as far as a client with no built onboarding can go.
  • しきい値 is drawn as the design's meter with two lines but operated by two native range inputs below it, not by dragging the lines. A hand-rolled drag could not be tested at all in a DOM-less environment, and a range gives the keyboard path for free.
  • 大きさ writes pal-scale's per-monitor store, so the slider and the pal's corner grip are literally one value (the module's own header asks for this). The ten lines resolving the current monitor are now duplicated between use-settings.ts and pal-container.tsx; the store is shared so there is no second source of truth, and folding them together means editing another issue's file this wave.
  • The 調整 key went on the station bar as an optional prop. It is the only band on screen at every size, and #12 removed the sidebar that used to carry a way in.
  • CLIENT_VERSION is a constant kept honest by a test that reads package.json, rather than a vite define (vitest would not supply it) or a JSON import (would need resolveJsonModule and risk a new typecheck error).

For denpa#35

The only mic-adjacent coupling is one prop. onThresholdsChange writes VADContext.updateSettings; when silero moves into Rust that becomes a different setter and nothing else in the sheet moves. A test pins that the sheet, its spec and its wiring call no browser media API.

#35 should also know why the threshold commit is shaped the way it is: review found that committing on every range onChange tore the mic down per event, because updateSettings (vad-context.tsx:265-273) does stopMic(); setTimeout(startMic, 100) whenever vadRef.current exists — one drag fired dozens of MicVAD.new cycles, and overlapping 100ms timers could leave the mic running when it was off. The sheet now holds a draft and commits once per gesture. That is a workaround for a real defect in updateSettings, not a fix for it. Any other caller still hits it.

Review findings and how they were handled

Twelve findings. Nothing was skipped outright. Fixed:

  • blocker settings-sheet.tsx:315 — per-onChange mic teardown, above. Now a pure ThresholdGesture reducer in settings-spec.ts, drawn from local state, committed exactly once on pointerup/keyup/blur.
  • major :561 — sheet at zIndex 4 buried the 画面共有 indicator (3) and any approval ask (band, 1), inverting precedence and hiding an ask that holds the mic. Sheet moved to SHEET_Z=2 under the indicator, and receiver-chrome withholds it entirely while approval !== null.
  • major :434 — 確認 committed a new address then called onProbe, whose closure captures the pre-change wsUrl, so every retune dialled the old station first. probePress() now retunes or probes, never both.
  • major :406 — 局 edited wsUrl only, leaving baseUrl (backgrounds, images, audio URLs) pointed at the previous host. httpOriginFrom() derives the http origin and use-receiver-chrome sets both on commit.
  • major use-settings.ts:42 — overlay permission read once at mount, latching 未許可 after the user granted it, against the invariant overlay-permission.ts:6-8 states. refreshOverlay() now re-runs on visibilitychange, window focus, and after openOverlaySettings() settles.
  • The seven minor findings (overlay row body copy, Escape/role="dialog"/focus-on-open, tab-role keyboard contract, reset copy promising a 待受 that is #28's, draft resync + Enter-to-commit, live-region announcement of the armed reset) were fixed as reported.

One finding was fixed differently from either option the reviewer named, and you should check my reasoning: major settings-sheet.tsx:384, 大きさ. The diagnosis is correct — PAL_SCALE_CSS_VAR is published only by PalContainer, which App.tsx renders in pet mode (:107-124); window mode renders <ReceiverChrome><Live2D/></ReceiverChrome> (:41-43) with no scale var, so the slider persists a value and prints a readout while she visibly does not change size in the mode the sheet lives in. I did not publish the var from useSettings (dead code — PalContainer and the receiver chrome are never mounted together, and a mode switch re-reads from storage anyway) and I did not have the receiver stage consume it (that gives the stored per-monitor pal scale a second meaning, a window zoom, which the design does not ask for). I also did not cut the row, because design § 5b explicitly defines it as "the same value as the pal's corner grip, saved per screen" — unlike 声, this one has a real producer. Instead SIZE_BODY now states the value applies to the desktop pal. That is honest copy, not a working control in this mode. If you'd rather the row moved her here, the ledger has the exact change.

Branch `issue/denpa-29-settings-sheet`, pushed. **Partially delivered — acceptance criteria 1 and 2 are UNMET.** Read the wire section before deciding to merge. ## What was built 調整, a four-tab settings sheet mounted inside the receiver stage, plus its wiring: - `src/renderer/src/components/receiver/settings-spec.ts` — all copy, key lists, reset levels, and the pure interaction logic (threshold clamps, the `ThresholdGesture` reducer, the reset key computation, the two-press arm). - `src/renderer/src/components/receiver/settings-sheet.tsx` — the sheet itself: 局 (address + upstream row + 端末承認 prose), 相手 (姿 cards via the existing `useSwitchCharacter`), 調整 (しきい値, 大きさ), 本体 (version, overlay permission, two reset levels). - `src/renderer/src/hooks/receiver/use-settings.ts` — address/threshold/scale/permission/reset state. - Additive changes only to `use-receiver-chrome.ts`, `station-bar.tsx`, `receiver-chrome.tsx`: the 調整 key is an optional prop mirroring #31's `onShareKey`, so a caller that passes nothing sees no change. Tests: **575 passed in 40 files** (`main` baseline 511 in 37). Typecheck is 585 errors, identical to the recorded `main` count, and grepping the output for every touched path returns nothing — no new errors. Lint remains broken on `main`'s missing `eslint-config-airbnb`; untouched. ``` Test Files 40 passed (40) Tests 575 passed (575) Duration 8.30s ``` One environment fix was needed to get there: the shared `node_modules` was missing `@chakra-ui/react`, `@emotion/react` and both `@chatscope` packages, so every `ChakraProvider` test failed to collect. Restored from the lock with `npm install --no-save`; `package.json` and `package-lock.json` are unmodified. ## What the tests cover The pure logic directly — threshold clamping (無音 >= 発話 is unrepresentable), the gesture reducer that decides when a threshold commits, which storage keys each reset level removes, the two-press arming. Plus structural pins: every `data-denpa-hit` across all four tabs is a natively focusable element (`button`/`input`/`a`/`select`/`textarea`) or the test fails; thresholds and 大きさ assert `type="range"`; toggles assert `type="checkbox"`; nothing carries `tabindex="-1"`; a source check fails if the component ever gains `onPointerDown`/`onMouseDown`/`onTouchStart`. A test pins that `CLIENT_VERSION` matches `package.json`. A test pins that the sheet, its spec and its wiring call no browser media API (ADR-0002). A test pins that no `denpa.*` frame name from `protocol.py` appears in anything this branch added. A test pins the absence of log export. ## Wire claims, with citations - The client-facing handler map is `interrupt-signal`, `mic-audio-data`, `mic-audio-end`, `raw-audio-data`, `text-input`, the three `*-response` answers, `screen-capture`, `fetch-configs`, `switch-config`, `fetch-backgrounds`, `request-init-config`, `heartbeat` — and **nothing** for a cron job list or a mute. `denpa/src/open_llm_vtuber/websocket_handler.py:66-83` - Full set of message types the go-between ever sends a client: `audio`, `backend-synth-complete`, `background-files`, `config-files`, `config-switched`, `control`, `error`, `force-new-message`, `full-text`, `heartbeat`, `heartbeat-ack`, `image`, `live2d-models/info`, `notice`, `request-init-config`, `set-model-and-conf`, `text-input`, `typing`, `user-input-transcription`, plus the three `*-request` asks. No voice, no TTS engine, no cron, no pairing state. (`grep -rn '"type": "' denpa/src/`) - `ASK_KINDS` is exactly three: approval, clarify, confirm, each with its `*-request`/`*-response`/`*_id` triple. `denpa/src/open_llm_vtuber/adapter_handler.py:59-63`. Not consumed here; verified because the prompt asserted it. - `upstream-status` carries a real boolean `attached`, pushed on connect and on every attach/detach. `websocket_handler.py:39` (type constant), `:134-148` (`upstream_report` / `broadcast_upstream_status`), `:167-169` (on connect). 局's upstream row reads this through `services/two-hop.ts`. - The model list is whole-config switching and nothing finer: `fetch-configs` → `{type: 'config-files', configs}` (`websocket_handler.py:479-487`), `switch-config` with `file` (`:489-496`), answered by `set-model-and-conf` carrying `model_info`, `conf_name`, `conf_uid`, `client_uid` and nothing else (`:171-181`). - `/client-ws` reads an **optional** `device_id` from the query string (`denpa/src/open_llm_vtuber/routes.py:37`) and the client never supplies one. No pairing enforcement exists anywhere in the go-between — `grep -rn 'pairing' denpa/src/` is empty. So 局's 端末承認 row is prose, not state. Note the prompt's flat "the client never sends a `device_id`" glosses over the route accepting one. - Hop one's frame vocabulary is `denpa.speak|notice|transcription|typing|image|approval|clarify|slash-confirm|text|voice|photo|interrupt|callback|turn-complete`, `denpa/hermes_plugin/denpa/protocol.py:45-62`. No job list, no mute. - `list_jobs()` / `pause_job()` / `resume_job()` are called by **nothing** in the go-between repo. `grep -rn 'list_jobs|pause_job|resume_job|cron' --include=*.py denpa/src denpa/hermes_plugin` returns only `hermes_plugin/denpa/adapter.py:17` (a docstring), `:638` and `:641` (the cron-tick hook), and `:699` (an env var name). ## The prompt was wrong about one thing, and it mattered The issue body and the design both say the 自発送信 cron list "is **not** cut" and enumerate the record fields, which reads as though it were available. It is not. Per the greps above: nothing produces a job list, no frame carries one or a mute, and the client handler map has no case for either. The ADR describes a decision that has not been implemented in the go-between. So the list was **left out**, and **acceptance criteria 1 (cron list with real schedules) and 2 (mute surviving restart) are unmet.** Building it would have been shipping a schedule UI with no producer. It needs a go-between issue for a job-list/mute message pair first, and #29 should be recorded as partially delivered rather than closed. ## Also deliberately left out - **声 readout** — no message anywhere names a voice or an engine. The issue's "声 is a readout, not a picker" holds by there being neither, per the design's rule that a deferred thing is omitted entirely rather than greyed out. - **A live pairing state in 局** — ADR-0004 is not implemented in the go-between, so 端末承認 explains where approval happens (in Hermes) rather than reporting a status. No key field. - **The 画面を見せる row that design § 5d puts in 電波** — it stayed on the station bar where #31 put it. The ask card draws over the stage and the sheet covers the stage, so a row here would either open a card the sheet hides or need a second copy of the card. Ledgered with the ~10-line fix. - **Deleting the orphaned upstream settings tree** (`components/sidebar/setting/*`, `hooks/sidebar/setting/*`, including `agent.tsx`'s `allowProactiveSpeak`/`idleSecondsToSpeak`, which ADR-0007 puts upstream). Nothing in 調整 renders any of it — which is what the issue asked — but the files are still on disk, unmounted since #12, and `use-general-settings.ts` still exports the two image-compression keys `use-media-capture.tsx` imports. Deletion is its own sweep and touching those files invites a sibling conflict. Ledgered. - **中継局 and 上流 version rows** in 本体 (design § 7a cuts them; nothing reports either), and **log export** (explicitly cut, absence pinned by a test). ## What could not be verified **Nothing here is visually confirmed.** I cannot see rendered output or drive a live turn. The sheet's 600px placement, the bottom-sheet geometry at 370px, whether the threshold lines land where the meter bars suggest, and the final z-order against #31's screen-share indicator are all reasoned from code, not seen. **More importantly: the vitest environment is `node`, and neither jsdom nor testing-library is installed, so no test in this repo can fire an event.** I could not click a key, press a key, or drag a slider. What I did instead, given the #19 warning, is the structural and pure-logic coverage listed above — every control is a natively focusable element, ranges give arrows and Home/End by construction, and the interaction logic that isn't the platform's is pure and tested directly. That is stronger than "a handler is attached", but it is not a keystroke reaching a control and I am not claiming it is. Also unverified: `onReset` calls `window.location.reload()`, which no test exercises; the Android overlay-permission row, which needs a real Android build to appear at all; and the address commit path, which needs a running go-between. ## Assumptions - **The two reset levels split as tuning vs tuning-plus-address.** § 7a puts the address in level one, but ADR-0004 removed the token from that list, and clearing the address at level one would leave 最初からやる with nothing to do. Level two is "back to 待受" as far as a client with no built onboarding can go. - **しきい値 is drawn as the design's meter with two lines but operated by two native range inputs below it,** not by dragging the lines. A hand-rolled drag could not be tested at all in a DOM-less environment, and a range gives the keyboard path for free. - **大きさ writes `pal-scale`'s per-monitor store,** so the slider and the pal's corner grip are literally one value (the module's own header asks for this). The ten lines resolving the current monitor are now duplicated between `use-settings.ts` and `pal-container.tsx`; the store is shared so there is no second source of truth, and folding them together means editing another issue's file this wave. - **The 調整 key went on the station bar as an optional prop.** It is the only band on screen at every size, and #12 removed the sidebar that used to carry a way in. - **`CLIENT_VERSION` is a constant kept honest by a test** that reads `package.json`, rather than a vite `define` (vitest would not supply it) or a JSON import (would need `resolveJsonModule` and risk a new typecheck error). ## For denpa#35 The only mic-adjacent coupling is one prop. `onThresholdsChange` writes `VADContext.updateSettings`; when silero moves into Rust that becomes a different setter and nothing else in the sheet moves. A test pins that the sheet, its spec and its wiring call no browser media API. #35 should also know why the threshold commit is shaped the way it is: review found that committing on every range `onChange` tore the mic down per event, because `updateSettings` (`vad-context.tsx:265-273`) does `stopMic(); setTimeout(startMic, 100)` whenever `vadRef.current` exists — one drag fired dozens of `MicVAD.new` cycles, and overlapping 100ms timers could leave the mic running when it was off. The sheet now holds a draft and commits once per gesture. **That is a workaround for a real defect in `updateSettings`, not a fix for it.** Any other caller still hits it. ## Review findings and how they were handled Twelve findings. **Nothing was skipped outright.** Fixed: - **blocker** `settings-sheet.tsx:315` — per-`onChange` mic teardown, above. Now a pure `ThresholdGesture` reducer in `settings-spec.ts`, drawn from local state, committed exactly once on pointerup/keyup/blur. - **major** `:561` — sheet at `zIndex` 4 buried the 画面共有 indicator (3) and any approval ask (band, 1), inverting precedence and hiding an ask that holds the mic. Sheet moved to `SHEET_Z=2` under the indicator, and `receiver-chrome` withholds it entirely while `approval !== null`. - **major** `:434` — 確認 committed a new address then called `onProbe`, whose closure captures the pre-change `wsUrl`, so every retune dialled the *old* station first. `probePress()` now retunes **or** probes, never both. - **major** `:406` — 局 edited `wsUrl` only, leaving `baseUrl` (backgrounds, images, audio URLs) pointed at the previous host. `httpOriginFrom()` derives the http origin and `use-receiver-chrome` sets both on commit. - **major** `use-settings.ts:42` — overlay permission read once at mount, latching 未許可 after the user granted it, against the invariant `overlay-permission.ts:6-8` states. `refreshOverlay()` now re-runs on `visibilitychange`, window focus, and after `openOverlaySettings()` settles. - The seven minor findings (overlay row body copy, Escape/`role="dialog"`/focus-on-open, tab-role keyboard contract, reset copy promising a 待受 that is #28's, draft resync + Enter-to-commit, live-region announcement of the armed reset) were fixed as reported. **One finding was fixed differently from either option the reviewer named, and you should check my reasoning:** **major** `settings-sheet.tsx:384`, 大きさ. The diagnosis is correct — `PAL_SCALE_CSS_VAR` is published only by `PalContainer`, which `App.tsx` renders in pet mode (`:107-124`); window mode renders `<ReceiverChrome><Live2D/></ReceiverChrome>` (`:41-43`) with no scale var, so the slider persists a value and prints a readout while she visibly does not change size in the mode the sheet lives in. I did not publish the var from `useSettings` (dead code — `PalContainer` and the receiver chrome are never mounted together, and a mode switch re-reads from storage anyway) and I did not have the receiver stage consume it (that gives the stored per-monitor pal scale a second meaning, a window zoom, which the design does not ask for). I also did not cut the row, because design § 5b explicitly defines it as "the same value as the pal's corner grip, saved per screen" — unlike 声, this one has a real producer. Instead `SIZE_BODY` now states the value applies to the desktop pal. **That is honest copy, not a working control in this mode.** If you'd rather the row moved her here, the ledger has the exact change.
aiko referenced this issue from a commit 2026-07-30 10:01:36 +00:00
Author
Owner

Shipped. Merged to main at 8504a2b (phase C3).

Closed as part of reconciling the board with main: the autonomous run's policy was not to auto-close, so shipped work stayed open and the tracker read as though nothing had landed. That policy is now reversed — issues close when they land.

Shipped. Merged to `main` at [`8504a2b`](https://git.aiko.works/aiko/denpa/commit/8504a2b) (phase C3). Closed as part of reconciling the board with `main`: the autonomous run's policy was not to auto-close, so shipped work stayed open and the tracker read as though nothing had landed. That policy is now reversed — issues close when they land.
aiko closed this issue 2026-07-30 13:32:58 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#29
No description provided.