thornhill-tv/AGENTS.md
2026-07-23 19:58:22 +02:00

12 KiB
Raw Permalink Blame History

AGENTS.md — Thornhill TV

Guidance for AI agents and contributors working in this repository.

What this project is

Thornhill TV is a desktop Electron app with a Vue 3 frontend. It presents a scrollable “space” of feed tiles and a year/month timeline overlay, controlled primarily by the keyboard. The UI is dark-themed, full-viewport, and framed as a TV-like surface rather than a conventional document app.

At a high level:

  • Electron owns the window, native theme, optional CORS bypass, and packaging.
  • Vue 3 owns the interactive UI (space grid + timeline).
  • Vite serves the UI in development and bundles it for production.
  • electron-builder produces platform installers (AppImage / NSIS / DMG).

Package metadata: thornhill-tv 0.1.0, MIT, entry start.js.

Architecture overview

┌─────────────────────────────────────────────────────────────┐
│  Node process (start.js dual role)                          │
│                                                             │
│  yarn start → start.js as Node                              │
│    ├─ Vite dev server (src-vue, random free port)           │
│    └─ spawn(electron, start.js) with PORT env               │
│                                                             │
│  start.js as Electron main                                  │
│    ├─ BrowserWindow + preload (src-electron/preload.js)     │
│    ├─ config.js → window chrome / CORS / title              │
│    └─ loadURL(localhost:PORT) | loadFile(dist/index.html)   │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│  Renderer (Vue 3)                                           │
│  App.vue                                                    │
│    ├─ Space.vue  → grid of Feed.vue tiles (scrollable)      │
│    └─ Timeline.vue → year / month navigator (Tab toggle)    │
└─────────────────────────────────────────────────────────────┘

Dual-entry bootstrap (start.js)

start.js is both the npm main and the Electron entry. Behavior depends on what require('electron') returns:

Context require('electron') Behavior
Plain Node (yarn start) Path string to the Electron binary Start Vite on an ephemeral port, spawn Electron with PORT set, exit when the child exits
Electron main process Electron API object Create the window, apply config, load the UI

This avoids a separate “main” vs “launcher” file.

Dev vs packaged load path

  • Development: renderer loads http://localhost:${PORT}/ from Vite. Vue DevTools (nhdogjmejiglipccpnnnanhbledajbpd) is installed when unpackaged. Detached DevTools open and stay active.
  • Packaged: renderer loads ./dist/index.html. DevTools are closed after open.

Native theme is forced to dark (electron.nativeTheme.themeSource = 'dark').

Build pipeline (build.mjs)

  1. Delete ./dist and ./build if present.
  2. Vite build of src-vue../dist with base: './' (file://-safe relative assets).
  3. electron-builder for the current host platform only:
    • Linux → AppImage (x64)
    • Windows → NSIS (x64)
    • macOS → DMG
  4. Output under ./build; build resources from src-electron/assets.
  5. Packaged file set is derived from the repo root listing, excluding .git, tooling files, and most .gitignore entries (but keeping dist, node_modules, and config.js in the packaged set when present).

Configuration (config.js)

Runtime config is required as ./config.js (gitignored). Copy from config.example.js:

Key Type Role
isSpellcheckEnabled boolean BrowserWindow spellcheck
isTitleBarEnabled boolean Native frame / title bar (frame)
isMenuBarEnabled boolean Menu bar visibility
isCorsEnabled boolean When false, strips real CORS headers and injects * allow-origin/methods/headers on all responses (useful for embedding remote media in feeds)
title string Window / HTML title (falls back to package name)

Agents changing window behavior should update config.example.js and document new keys here; never commit secrets in config.js.

Preload (src-electron/preload.js)

Currently a stub (console.log). The preload path is wired in BrowserWindow webPreferences.preload. Prefer exposing a narrow contextBridge API here if the renderer needs Node/Electron capabilities; do not enable nodeIntegration unless explicitly required.

Directory map

thornhill-tv/
├── AGENTS.md                 # This file
├── package.json              # Scripts, deps, Electron main = start.js
├── yarn.lock
├── start.js                  # Dev launcher + Electron main
├── build.mjs                 # Vite production build + electron-builder
├── config.example.js         # Template for config.js
├── config.js                 # Local overrides (gitignored)
├── src-electron/
│   ├── preload.js            # Preload script for the renderer
│   └── assets/
│       └── icon.png          # App / builder icon
├── src-vue/
│   ├── index.html            # Shell: destyle.css, dark color-scheme, Apple font
│   ├── main.js               # createApp(App) → .App
│   ├── static/fonts/
│   │   └── Apple.woff2
│   └── components/
│       ├── App.vue           # Layout + Tab focus switch
│       ├── Space.vue         # Feed grid + arrow-key pan
│       ├── Timeline.vue      # Year/month bar
│       └── Space/
│           └── Feed.vue      # Single feed tile (placeholder color)
├── dist/                     # Vite output (gitignored)
└── build/                    # electron-builder output (gitignored)

UI features and interaction model

Layout (App.vue)

  • Full viewport CSS grid: 1fr main area + 125px bottom strip for the timeline.
  • Two siblings: Space and Timeline.
  • Tab (on keyup): toggles isTimelineActive, focuses either Timeline or Space. Tab keydown is always prevented (no browser focus cycle).
  • Timeline receives :is-active to animate open/closed.

Space — feed mosaic (Space.vue)

  • Renders feedCount (currently 100) Feed children in a CSS grid.
  • Grid dimensions: columnCount = floor(sqrt(feedCount)), rowCount = ceil(feedCount / columnCount) so the mosaic stays roughly square.
  • Tile size via CSS variables: --feed-width: 400px, --feed-height: 300px, --feed-gap: 50px. Outer Space overflows hidden; the inner Space__Feeds is larger and is panned by scrolling the host.
  • Arrow keys while Space is focused: continuous pan via requestAnimationFrame loops (SCROLL_STEP = 25 px per frame). Separate flags for X/Y so diagonal motion works. Keyup clears the corresponding axis flag.
  • Host is tabindex="1" so it can receive keyboard focus.

Feed tile (Space/Feed.vue)

  • Placeholder: random hex background color per instance.
  • Intended extension point for real video / stream / channel content.

Timeline (Timeline.vue)

  • Collapsed by default (max-height: 0); when Timeline--active, expands with a 0.25s transition over a semi-transparent black bar.
  • Years: current year 5 through current year + 1 (seven slots). Default activeYear index is 5 (current year).
  • Months: single-letter labels J F M A M J J A S O N D. Month row appears only when activeMonth is a number.
  • Keyboard (on keyup, when Timeline focused):
    • Up: leave month mode (activeMonth = undefined).
    • Down: enter month mode (current calendar month if active year is current; otherwise month 0).
    • Left / Right: move month (011) or year index within bounds.
  • Active year/month use opacity (0.5 inactive, 1 active). Uses the custom Apple font face.

Visual / CSS conventions

  • Global reset: destyle.css.
  • color-scheme: dark; :focus-visible { outline: none } (keyboard focus is app-managed).
  • Prefer local <style> in SFCs; layout uses CSS grid and custom properties rather than a CSS framework.
  • Do not introduce Inter/Roboto/system defaults for branded chrome; the project already ships Apple.woff2 for timeline type.

Scripts and workflow

Command Purpose
yarn / yarn install Install dependencies
yarn start Vite + Electron (development)
yarn build Production Vite build + electron-builder for this OS

Prerequisites for agents:

  1. Ensure config.js exists (cp config.example.js config.js) before start or build.
  2. Use Yarn (lockfile is yarn.lock); do not switch package managers without an explicit request.
  3. Do not commit config.js, dist/, build/, or node_modules/.

Dependency roles

Package Role
electron Desktop shell
electron-builder Installers
electron-devtools-installer Vue DevTools in unpackaged runs
vite, @vitejs/plugin-vue, vite-plugin-html Dev server + HTML title injection + production bundle
vue UI framework (Options API via defineComponent)
destyle.css CSS reset
dayjs Declared dependency; not yet used in source — prefer it for any new date logic to match the package intent

Conventions for code changes

Prefer existing patterns

  • Vue components use Options API (defineComponent, data, methods, mounted / beforeUnmount). Match that style unless migrating the whole tree.
  • Keyboard handling is done with DOM listeners on the component root (or window in App), not Vue @keydown templates — keep that consistent for focus-scoped controls.
  • Event listener cleanup belongs in beforeUnmount.
  • Keep Electron concerns in start.js / src-electron; keep UI in src-vue.

Safe extension points

  • Real feeds: flesh out Feed.vue (and possibly props from Space.vue) without changing the grid math unless product requirements demand it.
  • Timeline ↔ Space coupling: today Timeline selection does not filter feeds; wiring year/month to feed data is a natural next feature.
  • IPC / APIs: add through preload.js + contextBridge, then consume from Vue.
  • Config toggles: extend config.example.js and destructure in start.js.

Avoid unless asked

  • Enabling nodeIntegration or disabling contextIsolation.
  • Committing generated dist/ / build/ artifacts.
  • Cross-compiling all platforms in one build (current builder only targets process.platform).
  • Rewriting the dual-role start.js without a clear migration plan — Vite port discovery and Electron spawn are tightly coupled.

Testing / verification

There is no automated test suite. After UI or Electron changes:

  1. yarn start and verify Tab focus switching, Space arrow panning, Timeline year/month navigation.
  2. For packaging changes, yarn build on the relevant OS and smoke-test the artifact under build/.

Mental model for agents

Treat Thornhill TV as a keyboard-first media wall: a large pannable grid of feeds plus a temporal navigator. Changes should preserve full-viewport immersion, dark theme, and arrow/Tab navigation. Electron is a thin host; almost all product behavior lives under src-vue/components.