Hub Split Plan¶
Split the two largest files — server/hub.py (2,011 lines) and static/js/audio.js (1,791 lines) — into focused modules.
Part 1: server/hub.py Split¶
Current state¶
hub.py is a monolith handling: app setup, lifespan, background loops, browser WS, wait WS, hook processing, HTTP routes (sessions, projects, agents, messages, inbox, notes, settings, usage, debug), and helper functions.
Target structure¶
| New file | Lines (est.) | What moves there |
|---|---|---|
routes.py |
~700 | All @app.get/post/put/delete HTTP endpoints |
websockets.py |
~250 | browser_websocket(), wait_websocket(), handle_browser_message(), send_to_browser(), _flush_browser_queue() |
hooks.py |
~200 | hook_tool_status(), _tool_activity_text(), _format_inbox_messages(), _inbox_write_and_notify() |
hub.py (slimmed) |
~500 | App creation, lifespan, background loops, shared state, imports |
Function assignments¶
routes.py¶
All HTTP route handlers, grouped by domain:
- Sessions:
list_sessions,spawn_session,terminate_session,restart_session,upload_file,set_project_status,mark_session_read,set_viewing_session,set_session_voice,set_session_speed - Projects:
list_projects,create_project,copy_project_history,activate_project,reorder_voices,rename_project,delete_project - Agents:
list_agents,get_agent,update_agent,assign_agent,regenerate_all_templates,regenerate_template - Messages:
send_message,speak_to_user,ack_message,reply_to_message,list_messages,get_message - Inbox:
get_inbox,peek_inbox - History:
get_history,clear_history - Settings:
get_settings,update_settings,_load_settings,_save_settings - Notes:
get_notes,update_notes - Services:
services_status,_load_whisper_model - Usage:
get_usage,get_context,_load_usage_sidecar,_save_usage_sidecar,_get_fallback_usage - Debug:
debug_info,debug_log,get_debug_log,debug_log(POST) - Shutdown:
shutdown_hub - Static:
index,static_file
Use APIRouter instances grouped by domain. Register them in hub.py via app.include_router().
websockets.py¶
browser_websocket()— browser WS at/wshandle_browser_message()— dispatches browser commandswait_websocket()— agent wait WS at/ws/wait/{session_id}send_to_browser()— broadcast to browser WS_flush_browser_queue()— drain queued browser messages
hooks.py¶
hook_tool_status()— PreToolUse/PostToolUse/Notification handler at/api/hooks/tool-status_tool_activity_text()— compose human-readable tool description_format_inbox_messages()— format inbox messages for additionalContext_inbox_write_and_notify()— write to inbox + notify browser/wait WS
hub.py (remains)¶
- FastAPI app creation, CORS, static mount
lifespan()— startup/shutdown- Background loops:
heartbeat_loop,compaction_monitor_loop,context_poll_loop,usage_poll_loop - Shared state:
sm(SessionManager),broker(MessageBroker),history(HistoryStore),agents_store,project_mgr,browser_ws,browser_queue - Helper:
_on_session_death,_hist_prefix,_gen_msg_id,_save_activity,_session_from_cwd,_resolve_session
Shared state access¶
The main challenge is shared mutable state. These globals live in hub.py and are needed by all modules:
sm: SessionManagerbroker: MessageBrokerhistory: HistoryStoreagents_store: AgentsStoreproject_mgr: ProjectManagerbrowser_ws: WebSocket | Nonebrowser_queue: asyncio.Queuelog: logging.Logger
Approach: Keep these as module-level globals in hub.py. Other modules import them:
Or use a lightweight HubState container class to avoid circular imports:
# hub_state.py (new, ~30 lines)
class HubState:
sm: SessionManager = None
broker: MessageBroker = None
# ... populated during lifespan()
state = HubState()
Part 2: static/js/audio.js Split¶
Current state¶
audio.js handles: waveform visualization, audio cues, TTS playback, karaoke word highlighting, VAD (voice activity detection), recording, mic/speaker selection, transport controls, push-to-talk, session state management, and thinking sounds.
Target structure¶
| New file | Lines (est.) | What moves there |
|---|---|---|
recorder.js |
~500 | Mic capture, recording, VAD, push-to-talk, mic/speaker selection |
player.js |
~600 | TTS playback, audio queue, karaoke, transport controls, thinking sounds |
audio.js (slimmed) |
~400 | Shared AudioContext, waveform, audio cues, session state, UI glue |
Function assignments¶
recorder.js¶
getMicStream(),populateMicSelector(),changeMicDevice(),populateSpeakerSelector(),changeSpeakerDevice()startRecording(),stopRecording(),cancelRecording(),sendAudio(),_flushPendingAudio(),sendSilentAudio()startVAD(),toggleVAD(),startPlaybackVAD(),stopPlaybackVAD(),startThinkingVAD(),stopThinkingVAD()toggleAutoInterrupt(),interruptPlayback()pttStart(),pttEnd(),_isTextTarget()updateMicUI(),MIC_SVG,MIC_SEND_SVG,MIC_INTERRUPT_SVG
player.js¶
playAudio(),enqueueAudio(),_playNextQueued(),_audioPlayQueueplayBufferedAudio(),padAudioBuffer(),getAudioPadSec()playMessageTTS(),stopTTSPlayback(),stopActiveAudio()karaokeSetupMessage(),_applyKaraokeSpans(),karaokeStart(),_karaokeFrame(),karaokeStop(),karaokeSeekTo(),karaokePlayFromWord(),_pendingKaraokeWords_wrapWordsInSpans(),_wrapTextNodesInKaraokeSpans()startThinkingSound(),stopThinkingSound(),toggleThinkingSounds()- Transport:
updateTransportBar(),transportPause(),transportNext(),transportPrev()
audio.js (remains)¶
audioCtx— shared AudioContext singletonplayTone(),cueListening(),cueProcessing(),cueSessionReady(),toggleAudioCues()- Waveform:
startWaveform(),drawWaveform(),stopWaveform(),waveCanvas,waveCtx - Session state:
setSessionState(),getSessionState(),_handleListeningUI(),_checkPendingListen() - Status stubs:
showStatusIndicator(),hideStatusIndicator(), etc.
Shared state access¶
These are currently module-level variables shared across functions:
audioCtx— the Web Audio context (stays inaudio.js, imported by others)sessionsMap — fromstate.js(already external)_audioPlayQueueMap — moves toplayer.js_pendingKaraokeWordsMap — moves toplayer.js- Recording state vars (
mediaRecorder,audioChunks, etc.) — move torecorder.js
Approach: Since these are plain <script> tags (no ES modules), all top-level variables are global. No import mechanism needed — just ensure load order in hub.html:
<script src="/static/js/state.js"></script>
<script src="/static/js/audio.js"></script> <!-- shared context, cues -->
<script src="/static/js/player.js"></script> <!-- playback, karaoke -->
<script src="/static/js/recorder.js"></script> <!-- capture, VAD -->
Migration strategy¶
- Extract, don't rewrite. Move functions verbatim — no refactoring during the split.
- One file at a time. Split
hub.pyfirst (Python has proper imports), thenaudio.js. - Test after each extraction. Verify the hub starts, browser loads, and voice pipeline works.
- Keep
state_machine.pyseparate as its own module.