11 KiB
AGENTS.md — Thornhill OS (v4)
Guidance for AI agents and contributors working in this repository.
What this project is
Thornhill OS is a web-based recreation of the Machine UI from the TV series Person of Interest. Version 4 merges the Machine Point-of-View (MPOV) chrome from classic Thornhill OS with the keyboard-first feed mosaic and timeline from Thornhill TV into one Deno Desktop app.
- Version: 4.0.0
- License: CC BY-NC-SA 4.0
- Stack: Deno Desktop · Bun · Vue 3 (
<script setup>) · Vite 8 ·destyle.css· commander.js - Docs (canonical product reference): sibling repo
thornhill-docs— do not add adocs/tree here
Terminology (see thornhill-docs Reference.md):
| Term | Meaning |
|---|---|
| Asset | People serving the Machine's purpose |
| Open system | Access to MPOV and communication from asset computers |
| Admin | The Machine's creator; controls the Machine via the terminal |
High-level architecture
┌─────────────────────────────────────────────────────────────┐
│ Deno Desktop │
│ • preload: src/preload.js (BrowserWindow + bindings) │
│ • HMR / packaging via `deno desktop` │
└──────────────────────────┬──────────────────────────────────┘
│ globalThis.bindings
┌──────────────────────────▼──────────────────────────────────┐
│ Vue 3 renderer (Vite) │
│ App.vue → Space + Timeline + Output + Terminal │
│ reactive store (src/store.js) │
└─────────────────────────────────────────────────────────────┘
There is no scriptable lib/ API, no default.js auto-run, and no @thornhill-corp/* modules in v4. UI components render store state; windows/output start empty until something future populates them.
Process / bootstrap
| Layer | Entry | Role |
|---|---|---|
| Desktop preload | src/preload.js |
Create Deno.BrowserWindow, bind shutdown |
| HTML shell | index.html → /src/main.js |
Mount Vue on #app, load destyle.css |
| Root UI | src/App.vue |
Layout + global shortcuts |
| State | src/store.js |
Shared reactive store |
| Commands | src/helpers/commands.js |
commander.js terminal dispatch |
Repository layout
thornhill-os-v4/
├── index.html # Shell: destyle, dark color-scheme, #app
├── package.json # Bun scripts + deps (commander, vue, destyle)
├── bun.lock
├── deno.json # Desktop app metadata + Vite tasks
├── vite.config.js # Vue plugin + node: shims for commander
├── scripts/build-desktop.sh # Multi-platform packaging → out/
├── .cursor/rules/
│ └── stack-and-tooling.mdc # Stack / Bun / run / build rules (always apply)
└── src/
├── main.js
├── App.vue # Grid layout + Space/Esc/F5/Tab
├── store.js
├── preload.js # Deno Desktop window + bindings.shutdown
├── bindings.d.ts # Types for globalThis.bindings
├── helpers/
│ ├── colors.js # Named palette + prop validator
│ └── commands.js # Terminal → commander (about/help/shutdown)
├── shims/ # Browser shims for commander’s node: imports
├── components/
│ ├── Space/ # Feed mosaic ↔ windows view
│ ├── Window/ # Machine window chrome + body widgets
│ ├── Terminal/ # Admin terminal overlay
│ ├── Timeline.vue
│ ├── Output.vue
│ └── LoadingTextDots.vue
└── static/ # Futura, Monaco, Apple fonts + images
UI model
Composition (App.vue)
Full-viewport CSS grid: 1fr main + 125px Timeline strip.
| Child | Role |
|---|---|
| Space | Feed mosaic (default) or Machine windows (after feed select) |
| Timeline | Year/month navigator (Tab toggle) |
| Output | Full-screen / overlay text with character reveal |
| Terminal | Admin overlay (v-if when open) |
Fonts: Futura (UI), Monaco (terminal), Apple (timeline). Design tokens as CSS variables on :root in App.vue (--color-*, --font-*).
Space: feed view ↔ windows view
| Mode | Visible | Background |
|---|---|---|
| feeds (default) | Feed mosaic only | Black |
| windows | SpaceWindows only |
Selected feed tile color |
Selection (feed view)
- Click tile → enter windows view
- ZQSD (AZERTY WASD): move selection cursor; highlight via
transform: scale(layout unchanged) - Enter → zoom into selected tile, then windows view
- After pan/
+/-zoom, if the selected tile leaves the viewport, selection moves to the nearest still-visible tile
Zoom
- Shared transform on the feeds layer (
translate+scale) +/-manual zoom in feed view- Enter/exit windows view reuses the same transform for zoom-in / zoom-out animation (~350ms)
Feeds are placeholders (random hex colors). Timeline does not filter feeds yet.
Escape / Space priority
- Terminal open → Esc closes terminal
- Else windows view → Esc exits to feeds (zoom out)
- Space key opens terminal from either view when closed
Keyboard map
| Context | Keys | Behavior |
|---|---|---|
| Global | Space | Open terminal (when closed) |
| Global | Escape | Close terminal, else exit windows → feeds |
| Global | F5 | Reload |
| Global | Tab | Toggle Timeline active + focus Space ↔ Timeline |
| Feed view (Space focused) | Arrows | Continuous pan |
| Feed view | Z / Q / S / D | Move feed selection |
| Feed view | Enter | Enter windows for selection |
| Feed view | Click tile | Select + enter windows |
| Feed view | + / - |
Zoom in / out |
| Timeline focused | Arrows | Year / month navigation |
| Terminal entry | Enter, ↑/↓, Ctrl+L, Ctrl+C | Submit, history, clear, cancel |
Ignore feed navigation / zoom while the terminal is open or Timeline is focused (except Esc / Space / F5 / Tab as above). Prefer DOM listeners on window / component roots (not template @keydown for focus-scoped controls). Clean up in onBeforeUnmount.
Terminal (commander.js)
UI: Terminal.vue + TerminalMainEntry.vue — overlay chrome, # comments, trailing &, history in store.history.
Dispatch: src/helpers/commands.js builds a commander Command per line (exitOverride, capture writeOut/writeErr). Native commands only:
| Command | Behavior |
|---|---|
about |
Thornhill O.S. v${version} from package.json |
help |
Lists about, help, shutdown |
shutdown |
await globalThis.bindings.shutdown() (Deno Desktop quit via preload) |
Unknown command → Command not found. No run, no module loading.
Commander in the browser: Vite aliases node:* imports to src/shims/* so commander bundles for the webview. Keep those shims if commander stays a dependency.
Store (src/store.js)
Reactive module (no Vuex / Pinia):
{
spaceSize: { width, height },
output: string[],
isOutputOverlay: boolean,
windows: [], // plain objects bound into Window.vue
history: [], // terminal history
viewMode: 'feeds' | 'windows',
selectedFeedIndex: number | null,
activeFeedColor: string | null,
isTerminalOpen: boolean,
isTimelineActive: boolean,
}
Helpers: setSpaceSize, clearOutput, replaceOutput, appendOutput, addWindow, removeWindow, clearWindows.
Zoom / pan offsets live in Space.vue (local), not the store.
Window components
Machine windows under src/components/Window/. Body widgets are resolved via a component map + <component :is> in Window.vue (not string-global registration).
Body types: Separator, Subtitle, Text, Picture, Meter, Table, Countdown, ScrollingData.
Colors must use the named palette in helpers/colors.js (transparent, black, white, gray, darkgray, darkergray, red, yellow, green, blue). Header progress and subtitle remain mutually exclusive.
Vue / code conventions
- Vue 3
<script setup>Composition API only (match the stack rule). - No Electron, React, or a second bundler unless explicitly requested.
- Prefer Bun for dependency changes (
bun add/bun remove); keepbun.lock; do not add npm/yarn lockfiles. - Product look-and-feel and lore live in thornhill-docs; keep this repo’s UI show-accurate.
- Do not reintroduce
lib/,default.js, or@thornhill-corp/*module loading unless the product direction changes explicitly. - License is non-commercial (CC BY-NC-SA 4.0).
Development workflow
Stack details: .cursor/rules/stack-and-tooling.mdc.
bun install # first time / after lockfile changes
bun run start # deno desktop --hmr --preload ./src/preload.js .
bun run build # all platforms → out/
bun run build-linux # (also build-macos / build-windows)
deno task dev / deno task build are Vite-only; prefer bun run start for desktop HMR.
Output basenames under out/ must not contain dots (Deno webview launcher strips after the last .).
Verification (no automated suite)
After UI or keyboard changes, manually check:
- Feed pan,
+/-zoom, ZQSD selection scale, viewport re-home - Enter/click → zoom into windows (bg = tile color); Esc → zoom out
- Tab timeline; Space/Esc terminal;
about/help/shutdown; F5
Do not commit
node_modules/, dist/, out/, .deno*.
Quick orientation map
| Task | Start here |
|---|---|
| Feed mosaic / zoom / selection | src/components/Space/Space.vue, Feed.vue |
| Machine window chrome / body widgets | src/components/Window/** |
| Output animation | src/components/Output.vue |
| Terminal UX / commands | src/components/Terminal/**, src/helpers/commands.js |
| Timeline | src/components/Timeline.vue |
| Global shortcuts / layout | src/App.vue |
| Shared state | src/store.js |
| Desktop quit / window | src/preload.js, src/bindings.d.ts |
| Stack / deps / packaging | .cursor/rules/stack-and-tooling.mdc, package.json, deno.json |
| Product docs / screenshots | thornhill-docs (sibling repo) |