Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Section Map — AI Agent Guide

Draws a minimap bar of the current song's structure over the player, with
click/wheel seeking and a per-section "glass fill" difficulty indicator.
Frontend-only — there's no `routes.py`; all logic lives in `screen.js`,
reading state straight off the Host's `window.highway` object.

## Plugin-spec compliance (see got-feedBack/feedBack-plugin-spec)

- **Folder name must equal `plugin.json`'s `id` exactly** (case-sensitive:
`section_map`) — a mismatch is a silent skip at plugin discovery.
- **Idempotent script guard, already in place:** the `playSong`/`showScreen`
wrapping and the `setInterval` poller are all installed inside a single
`window.__slopsmithSectionMapHooksInstalled` guard at the bottom of
`screen.js`. The Host may re-execute `screen.js` on plugin reload — any
new top-level listener/timer needs to go inside that same guard, not a
bare call alongside it.
- **No bespoke API with `feedback-plugin-dynamic-difficulty`.** Both plugins
independently read the same Host surface (`highway.getPhrases()` /
`hasPhraseData()` / `getMastery()`) for section-difficulty data — this
plugin works whether or not dynamic_difficulty is installed, as long as
*something* populates that phrase data. Don't reach into
dynamic_difficulty's own globals/localStorage directly; go through
`window.highway`.
- **Seeking must go through the Host's canonical funnel**
(`window.feedBack.seek` / `window.slopsmith.seek`, wrapped by `_smSeek`),
not by poking `audio.currentTime` directly — see the comment on `_smSeek`
for why that breaks under native/streaming playback backends. The direct
`audio.currentTime` path only exists as a last-resort fallback for a Host
old enough to lack the seek API.
- **No `MutationObserver`, no DOM polling for section data** — `_smUpdate`
reads `highway.getSections()`/`getSongInfo()`/`getTime()` directly and
only re-renders the bar when the sections reference actually changes.

## Versioning

Bump `version` in `plugin.json` whenever a change is user-visible — a
rendering fix, a new interaction (click/wheel/hover), a changed setting
(best-practices rule 4: bump on every release; the plugin manager uses
this to detect updates). Patch (`1.x.y`) for fixes, minor (`1.x.0`) for
new features, matching normal semver conventions.
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "section_map",
"name": "Section Map",
"version": "1.2.0",
"version": "1.2.1",
"private": false,
"script": "screen.js",
"category": "practice",
Expand Down
31 changes: 25 additions & 6 deletions screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ let _smBar = null;
let _smSections = [];
let _smDuration = 0;

// Most-specific-first: 'pre' must come before 'chorus'/'verse' so a
// "pre-chorus"/"pre-verse" section name (which contains both substrings)
// matches its own color instead of falling through to the base section's.
const SM_COLORS = {
'pre': '#84cc16',
'noguitar': '#374151',
'breakdown': '#f97316',
'riff': '#06b6d4',
'intro': '#3b82f6',
'verse': '#22c55e',
'chorus': '#eab308',
'bridge': '#a855f7',
'solo': '#ef4444',
'outro': '#6b7280',
'breakdown': '#f97316',
'riff': '#06b6d4',
'pre': '#84cc16',
'noguitar': '#374151',
'default': '#4b5563',
};

Expand Down Expand Up @@ -186,11 +189,16 @@ function _smRender() {
const color = _smGetColor(sec.name);

// Clean up section name for display
const ordinalMatch = sec.name.match(/(\d+)$/);
let label = sec.name.replace(/\d+$/, '').trim();
label = label.charAt(0).toUpperCase() + label.slice(1);
// The on-bar label drops the trailing digit (no room for "Verse 2" in
// a ~9px-tall strip), but the hover title keeps it — otherwise two
// "Verse" blocks are indistinguishable on mouseover.
const titleLabel = ordinalMatch ? `${label} ${ordinalMatch[1]}` : label;

html += `<div class="sm-block" style="position:absolute;left:${startPct}%;width:${widthPct}%;top:0;bottom:0;background:${color};border-right:1px solid rgba(0,0,0,0.3);display:flex;align-items:center;justify-content:center;overflow:hidden;transition:opacity 0.15s;"
title="${label} (${_smFmt(sec.time)})">
title="${titleLabel} (${_smFmt(sec.time)})">
<span style="font-size:9px;color:rgba(255,255,255,0.8);white-space:nowrap;text-overflow:ellipsis;overflow:hidden;padding:0 3px;">${label}</span>
<div class="sm-glass-slot" style="position:absolute;inset:0;pointer-events:none;"></div>
</div>`;
Expand Down Expand Up @@ -325,7 +333,18 @@ if (typeof module !== 'undefined' && module.exports) {
if (window[HOOK_KEY]) return;
window[HOOK_KEY] = true;

// Poll for updates
// Poll for updates. sectionmap#3 asked to replace this with an event
// listener once highway emits a sections-changed event — highway has no
// such event (sections arrive over the WS with no dedicated signal), but
// it does emit 'song:ready' once section/phrase/etc. data has fully
// landed, so listen for that to build the bar immediately instead of
// waiting up to 200ms after a song loads. The interval still has to stay:
// the playback-position marker/highlight/glass-fill refresh every tick
// need continuous updates tied to the audio clock, which isn't something
// an event can replace without just re-inventing polling.
if (window.feedBack && typeof window.feedBack.on === 'function') {
window.feedBack.on('song:ready', _smUpdate);
}
setInterval(_smUpdate, 200);

// Hook into playSong
Expand Down