**jms:** new session. **Claude:** I'll pull up the session log and site notes. Here's your briefing. ## Last session — 2026-09-08 (6): TDPyMovieplayer Phase 2 One clip now plays on its own display (Movie File In → null → Window COMP on display 2, centered, exclusive). The interesting part was that the phase's planned deliverable — a written-down table of parameter names — turned out to be the wrong shape of answer twice: - **Names are looked up, not transcribed.** TouchDesigner ships `Config/TDParameterHelp.json` (5.4 MB, regenerated per build); `scaffold/params.py` reads it and searches help text as well as names. - **Menu tokens are the gap** that file doesn't cover, so `set_menu()` resolves against the live parameter — and refused `"Single Monitor"` on `justifyoffsetto`, listing the three real options instead. - **`COMP.create()` silently appends a digit** even when the name is free; `startup.create()` takes the name back. The rebuild button had been at `/project1/rebuild1` unnoticed since it was written. - **A DPI measurement I got wrong:** the displays are mixed (3840×2160 @150% beside two 2560×1440 @100%), not uniform — my first query used system-DPI awareness instead of per-monitor v2. `dpiscaling: native` cleared the warning. Also: framework Phase 3 (the build layer) is still unwritten, but now with an argument behind it — the four helpers real projects have asked for are all about *knowing and reporting*, not describing networks. Two roadmap items added at your request (Phase 8 perform window mode, rewritten Phase 5), with Phase 8 correctly preceding Phase 5 despite the numbering. ## Open items in Current Work 1. **[[Glitch (Cubitt)|Sean Cubitt's "Glitch"]]** article (240.10) — in progress. 2. **Review the publish backlog** — 344 unpublished vs. 273 published, measured 2026-09-08. 3. **obsidian-headless bug report** at [390.9980.0010](300-Code/390 - Claude Code/9980 - Bug Reports/0010 - obsidian-headless readdirSync Reparse Points.md) — drafted, awaiting your review, submit upstream if wanted. 4. **Manually add `300 - Code` and `160 - Software` to the site index.** 5. **TouchDesigner-Python framework** — Phases 0–2 done (`52f843e`); Phase 3 build layer still open. 6. **TDPyMovieplayer** — Phase 2 complete (`0d9e339`, `965125c`). Next up: **Phase 4, the cycle** — shuffled deck, random cue point, random speed, fixed dwell. Working tree is clean. Where do you want to start? **jms:** lets work on TDPyMovieplayer. review project documentation to come up to speed. lets work on phase 5. I want to create a control panel in touchdesigner that opens in a new window. For now, the control panel should have a play, pause, and next button that controls the state of the movie file in TOP. The control panel should be a sibling of "generated" and "window" and should open in a bounded window 'controlPanelWindow' on a different display than 'window'. actually lets rename 'window' to 'videoPlayerWindow'. **Claude:** I'll read the project documentation first. Documentation read. Now let me look up the actual parameter names rather than guess — the framework ships `scaffold.params` for exactly this. I have everything I need. Here's what I found, and three decisions I want your call on before I write anything. **The shape of it.** `videoPlayerWindow` and `controlPanelWindow` both sit beside `generated`, converged on rather than recreated — that's the Phase 2 reasoning (a window inside `generated` would close and reopen on every rebuild). But the *panel itself* I'd build inside `build()` and destroy/recreate each time, so iterating on it is one click of the rebuild button. That's safe here in a way it isn't for the rebuild button: nothing on the control panel calls `build()`, so no DAT destroys itself mid-callback. Transport functions go in a new `tdpy/player.py` — `play()`, `pause()`, `next_clip()` — with the button callbacks as two-line shims, which is the plan's one non-negotiable rule and gives Phase 4's `advance()` its home. **Parameter names are looked up, not guessed** — `scaffold.params` gave me `winw`/`winh`/`borders`/`size` on the Window COMP and `align`/`spacing` on the Container COMP. The menu tokens for `size` ("Custom") and `align` ("Left to Right") aren't in that table, so I'll write them as labels and let `set_menu` report what they resolve to, same as Phase 2. **jms:** Your questions have been answered: "Which display should controlPanelWindow open on?"="Display 3 (Recommended)", "What should the 'next' button do at this phase?"="Next playlist row, stateless (Recommended)", "Play and pause as two buttons, or one toggle?"="Two momentary buttons (Recommended)". You can now continue with these answers in mind. **Claude:** All three recommendations taken. Writing the code now — starting with the new transport module. Now `build.py`. First the constants: Now the build function and the window helper: All parameters confirmed against the shipped table. Let me review the finished build flow: Phase 5 is written. Three files changed, one added, and the existing 31 tests plus 18 new ones pass (`49 passed`). **[tdpy/player.py](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer/tdpy/player.py)** — new. `play()`, `pause()`, `next_clip()`, each callable from the textport with no arguments, which is the project's one non-negotiable rule. `next_clip()` keeps no state: it reads the player's current `file`, finds that row in the playlist table, and loads the following one. A path that isn't in the table answers row 0, which covers both ways that happens — an empty `file` on a fresh build, and a clip deleted from `media/` since the last scan. This is where Phase 4's `advance()` goes. **[tdpy/build.py](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer/tdpy/build.py)** — `window` → `videoPlayerWindow`, and `_add_window` split into a shared `_ensure_window` with two callers, since the two windows now differ in size, borders and display but agree on everything else. The panel is a `containerCOMP` at `/project1/controlPanel` holding three momentary buttons laid out left-to-right, plus one Panel Execute DAT watching all three — `panelValue.owner.name` is the dispatch key, so `player.COMMANDS` and `build.CONTROL_BUTTONS` are the only two places a control is named. A test asserts those two sets are equal, which is the one thing nothing at runtime would catch until a button was pressed and did nothing. Three things worth naming: - **The panel is rebuilt on rebuild; the window isn't.** `controlPanelWindow` is converged onto like the video window, but the panel inside it is destroyed and recreated by `build()` — so relabelling a button is a rebuild click, not a restart. Safe here for the reason it isn't for the rebuild button: nothing on this panel calls `build()`. - **A retired-name sweep.** `LEGACY_NAMES = ("window",)` gets destroyed each build. A renamed Window COMP doesn't rename the one a running session or a saved `.toe` already holds — it leaves the old one sitting there holding display 2 exclusively while the new one asks for the same display. Removable once no session can be carrying one. - **Missing displays are now reported.** `_display_attached()` checks the request against `len(td.monitors)` and falls back to `primarydisplay` with a log line, rather than a window silently opening somewhere unexpected. Two things I can't settle without a running TouchDesigner. The Panel Execute DAT's `panels` parameter is set to a space-separated list of the three button paths — documented to accept a list, but if clicks do nothing that's the first suspect, and it's a one-line change to three DATs. And the framework's known `activeViewer` bug may simply not bite here, since panel interaction inside a Window COMP goes through the window rather than the node's viewer flag — `interact` is set explicitly on the window for that reason. Launching TouchDesigner takes display 2 exclusively, so I'd rather not do that behind you. Want to run `open-project.bat` and click the three buttons? I'll read `logs/startup.log` after — it'll have the resolved menu tokens for `size` and `align`, which are the two I wrote as UI labels. **jms:** it works, except the initial display for controlPanelWindow was invalid. it works properly when set to display 1. **Claude:** Let me look at the log and at what the `display` parameter actually means. Everything else worked — `panels` took the list (clips advanced 2→3→4), and the menus resolved to `horizlr` and `custom`. The display guard is the bug: it accepted 3 and would have rejected 0. The help calls it a display **index**, which points at 0-based. Let me confirm rather than infer. Two decisive strings in the binary. Let me get their full context. Confirmed, and my guard was wrong on both ends. TouchDesigner's own string is `Monitor specified in <op> does not exist. Opening on monitor 0 instead.` — monitor **0** is a real monitor, so the parameter is a 0-based index into the Monitors DAT, not a 1-based display number. My check accepted 3 (out of range) and would have rejected 0 (the primary). Now a test for the thing that was actually wrong — `_display_attached` takes `td` as an argument, so it's testable at a prompt: Fixed, and the suite is at 60 passing. **The numbering.** The Window COMP's `display` is a zero-based index into the Monitors DAT, not the display number Windows shows. The evidence is TouchDesigner's own out-of-range message in `libTD.dll` — `Monitor specified in <op> does not exist. Opening on monitor 0 instead.` — since falling back onto monitor 0 only makes sense where 0 is a monitor. So on your three displays the valid values are 0, 1, 2. `CONTROL_WINDOW_DISPLAY` is now `1`, matching what you found by hand. **My guard was wrong in the same way the constant was**, which is why nothing warned: `_display_attached` was written as `1 <= display <= count`, so it passed the display 3 that doesn't exist and would have refused display 0, the primary. It's now `0 <= display < count`, and the message reports the valid range rather than only the count — a bare "3 attached" is what made 3 look reasonable to begin with. That function takes `td` as an argument rather than importing it, so it's testable at a prompt: **[tests/test_build.py](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer/tests/test_build.py)** is new, covering both halves of the bug plus `_panel_width`, and asserting the two windows never land on the same display — the requirement the phase exists for, and the one thing a later edit could quietly break. Also worth recording from your run: `panels` did accept the space-separated list (the log shows clips advancing 2→3→4), `align` resolved to `horizlr`, `size` to `custom`, and the retired `/project1/window` was found and removed. The `activeViewer` bug didn't bite — panel interaction through the window works. Since `_ensure_window` converges rather than recreates, one click of rebuild will move the panel window to display 1 from code. Worth doing to confirm the log comes back clean with no display warning — then I'll commit both repos. **jms:** done **Claude:** Clean — display 1, no warning. Committing the project repo first so the vault docs can cite the hash. Pushed as `cecf48c`. Now the vault pages — Phase 5's record is part of the deliverable. Now the code-side page: Phase 5 is done and recorded. Summary of where things landed: **Code** — `cecf48c`, pushed to [TDPyMovieplayer](https://github.com/wtkns/TDPyMovieplayer). New `tdpy/player.py`, control panel and window in `build.py`, `window` → `videoPlayerWindow`, 60 tests all running outside TouchDesigner. **Vault docs updated:** - [160.0020](100-Projects/160 - Software/0020 - TDPyMovieplayer.md) — Phase 5 written up as complete, including why it was scoped down (four of its five planned controls are parameters of a cycle that doesn't exist yet, so building them would have meant building Phase 4's panel before Phase 4). Seeding moved to Phase 4, where the sequence it seeds lives. Phase 8's "build me before Phase 5" constraint dropped — it was ignored without cost, because the panel is placed relative to a display rather than to another window. - [330.0020](300-Code/330 - TouchDesigner/0020 - TDPyMovieplayer.md) — three new working-notes sections: the zero-based Display index and how it was settled out of `libTD.dll`; the rebuild-the-panel/converge-the-window split and why it's safe here but not for the rebuild button; and the retired-name sweep. The `activeViewer` open issue is struck through — it was predicted to bite this phase and didn't. - [010 - Site Notes.md](010 - Site Notes.md) — `Current Work` now reads Phase 5 complete, Phase 4 next. One thing I'd flag as the session's most transferable finding: the guard I wrote to catch a bad display number shared the bad number's own wrong assumption, so it reported clean on the error it existed to catch. That's the second time on this project — the Phase 2 DPI measurement failed the same way. Both are now written down on the code page as a shape of failure, not just as two facts. I haven't run the full **update and push** — no session log entry, transcript, memory update, or `ob sync`. Say the word and I'll do all four. **jms:** update and push **Claude:** Running the full sequence. Session log first: Now the transcript: