Settings sheet — 電波 / 相手 / 局 / 本体 #29
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
v2
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wayfinder:ticket
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiko/denpa#29
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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, andpause_job()/resume_job()is the mute: no dashboard HTTP, no session token, no relay (ADR-0007). Records already carryid,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.mdand 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
hermes cron listReference
ADR-0004, 0007, 0009. Design: 3b, 3c, 5b, 7a.
Branch:
issue/denpa-29-settings-sheet(offmain).npm testis 559 passed in 40 files (main was 511 in 37).npm run typecheckstill 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.onScanlineToggle#12left with no control. No per-setting ASR or TTS: whole-config switching is the only mechanism there is.switch-config, the real model mechanism), and 大きさ bound topal-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.two-hop's model (#27), and the 経路 note.package.jsonby a test), repo and Cubism license links, the Android overlay permission row drivingopenOverlaySettings()— 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
list_jobs()/pause_job()are in-process and the records carry every field 局 draws — but nothing has been written.hermes_plugin/denpa/adapter.pynever calls them (only a docstring at :17 and the tick hook at :638),protocol.py:45-62defines 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 callingcron.jobs, two client message types.set-model-and-confcarriesmodel_info,conf_name,conf_uid,client_uidand 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.device_id(routes.py:37reads one from the query string; the client never supplies it), so there is no approved/pending to draw.Assumptions worth checking
onThresholdsChangewritesVADContext.updateSettings, which is theMicVADstore; 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 everydata-denpa-hiton all four tabs and fails if any is not abutton/input/a, the thresholds and the scale asserttype="range"(arrows and Home/End for free), the toggles asserttype="checkbox", nothing carriestabindex="-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, 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, theThresholdGesturereducer, 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 existinguseSwitchCharacter), 調整 (しきい値, 大きさ), 本体 (version, overlay permission, two reset levels).src/renderer/src/hooks/receiver/use-settings.ts— address/threshold/scale/permission/reset state.use-receiver-chrome.ts,station-bar.tsx,receiver-chrome.tsx: the 調整 key is an optional prop mirroring #31'sonShareKey, so a caller that passes nothing sees no change.Tests: 575 passed in 40 files (
mainbaseline 511 in 37). Typecheck is 585 errors, identical to the recordedmaincount, and grepping the output for every touched path returns nothing — no new errors. Lint remains broken onmain's missingeslint-config-airbnb; untouched.One environment fix was needed to get there: the shared
node_moduleswas missing@chakra-ui/react,@emotion/reactand both@chatscopepackages, so everyChakraProvidertest failed to collect. Restored from the lock withnpm install --no-save;package.jsonandpackage-lock.jsonare 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-hitacross all four tabs is a natively focusable element (button/input/a/select/textarea) or the test fails; thresholds and 大きさ asserttype="range"; toggles asserttype="checkbox"; nothing carriestabindex="-1"; a source check fails if the component ever gainsonPointerDown/onMouseDown/onTouchStart. A test pins thatCLIENT_VERSIONmatchespackage.json. A test pins that the sheet, its spec and its wiring call no browser media API (ADR-0002). A test pins that nodenpa.*frame name fromprotocol.pyappears in anything this branch added. A test pins the absence of log export.Wire claims, with citations
interrupt-signal,mic-audio-data,mic-audio-end,raw-audio-data,text-input, the three*-responseanswers,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-83audio,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*-requestasks. No voice, no TTS engine, no cron, no pairing state. (grep -rn '"type": "' denpa/src/)ASK_KINDSis exactly three: approval, clarify, confirm, each with its*-request/*-response/*_idtriple.denpa/src/open_llm_vtuber/adapter_handler.py:59-63. Not consumed here; verified because the prompt asserted it.upstream-statuscarries a real booleanattached, 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 throughservices/two-hop.ts.fetch-configs→{type: 'config-files', configs}(websocket_handler.py:479-487),switch-configwithfile(:489-496), answered byset-model-and-confcarryingmodel_info,conf_name,conf_uid,client_uidand nothing else (:171-181)./client-wsreads an optionaldevice_idfrom 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 adevice_id" glosses over the route accepting one.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_pluginreturns onlyhermes_plugin/denpa/adapter.py:17(a docstring),:638and: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
components/sidebar/setting/*,hooks/sidebar/setting/*, includingagent.tsx'sallowProactiveSpeak/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, anduse-general-settings.tsstill exports the two image-compression keysuse-media-capture.tsximports. Deletion is its own sweep and touching those files invites a sibling conflict. Ledgered.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:
onResetcallswindow.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
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 betweenuse-settings.tsandpal-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.CLIENT_VERSIONis a constant kept honest by a test that readspackage.json, rather than a vitedefine(vitest would not supply it) or a JSON import (would needresolveJsonModuleand risk a new typecheck error).For denpa#35
The only mic-adjacent coupling is one prop.
onThresholdsChangewritesVADContext.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
onChangetore the mic down per event, becauseupdateSettings(vad-context.tsx:265-273) doesstopMic(); setTimeout(startMic, 100)whenevervadRef.currentexists — one drag fired dozens ofMicVAD.newcycles, 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 inupdateSettings, 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:
settings-sheet.tsx:315— per-onChangemic teardown, above. Now a pureThresholdGesturereducer insettings-spec.ts, drawn from local state, committed exactly once on pointerup/keyup/blur.:561— sheet atzIndex4 buried the 画面共有 indicator (3) and any approval ask (band, 1), inverting precedence and hiding an ask that holds the mic. Sheet moved toSHEET_Z=2under the indicator, andreceiver-chromewithholds it entirely whileapproval !== null.:434— 確認 committed a new address then calledonProbe, whose closure captures the pre-changewsUrl, so every retune dialled the old station first.probePress()now retunes or probes, never both.:406— 局 editedwsUrlonly, leavingbaseUrl(backgrounds, images, audio URLs) pointed at the previous host.httpOriginFrom()derives the http origin anduse-receiver-chromesets both on commit.use-settings.ts:42— overlay permission read once at mount, latching 未許可 after the user granted it, against the invariantoverlay-permission.ts:6-8states.refreshOverlay()now re-runs onvisibilitychange, window focus, and afteropenOverlaySettings()settles.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_VARis published only byPalContainer, whichApp.tsxrenders 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 fromuseSettings(dead code —PalContainerand 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. InsteadSIZE_BODYnow 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.Shipped. Merged to
mainat8504a2b(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.