mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-01 23:12:29 +00:00
Extension backrooms (#2348)
* new extension backrooms-canvas * new extension backrooms-canvas * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * update according to code review update according to code review * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * apply code review, change backviews to backrooms * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -160,6 +160,12 @@
|
||||
"description": "Quickly swipe through backlog issues to triage decisions like assign, needs-info, defer, close, or ignore.",
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"source": "extensions/backrooms-canvas",
|
||||
"description": "Wander an endless first-person backrooms in a Copilot canvas while agents work; their status ghost-writes on the walls.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"name": "cast-imaging",
|
||||
"source": "plugins/cast-imaging",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"description": "Wander an endless first-person backrooms in a Copilot canvas while agents work; their status ghost-writes on the walls.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "John Haugabook",
|
||||
"url": "https://github.com/jhauga"
|
||||
},
|
||||
"keywords": [
|
||||
"backrooms",
|
||||
"copilot-canvas",
|
||||
"interactive-canvas",
|
||||
"first-person",
|
||||
"procedural-generation",
|
||||
"session-breaks"
|
||||
],
|
||||
"logo": "assets/preview.png",
|
||||
"extensions": "."
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
# BackRooms Canvas
|
||||
|
||||
A GitHub Copilot canvas that opens an endless first-person backrooms in the side panel. Yellow-wallpapered halls under humming fluorescent panels, drop-tile ceilings, and worn office carpet, filmed through the shake and grain of a 1990-era handheld camcorder. Somewhere past the fog, something else walks the same halls.
|
||||
|
||||
It is a canvas port of the [BackViews VSCode extension](https://github.com/isocialPractice/vscode-backviews). The world is powered by [cmd-backedges](https://github.com/isocialPractice/cmd-backedges), a procedural infinite maze engine, so every wall, room, and prop is a pure function of the seed: the same seed always rebuilds the same rooms, forever, in every direction.
|
||||
|
||||
While an agent works in the session, the halls start writing back. Its status and streaming responses are scrawled onto the walls in a messy ink script, and a camcorder-style token counter runs under the HUD battery.
|
||||
|
||||
## Files
|
||||
|
||||
- `extension.mjs` — canvas declaration, loopback game server, static asset handling, and agent actions.
|
||||
- `game/` — the prebuilt game bundle (`webview.js`) and the `index.html` host shim served inside the canvas.
|
||||
- `materials/` — photo textures (`wallpaper.jpg`, `ceiling.jpg`, `carpet.jpg`) tiled over the procedural atlas.
|
||||
- `assets/` — app icon and `preview.png` for the extensions gallery.
|
||||
- `package.json` — declares the Copilot SDK dependency and ESM entry point.
|
||||
- `copilot-extension.json` — Copilot extension name/version metadata.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 20.19 or newer**, because the Copilot SDK requires `node ^20.19.0 || >=22.12.0`.
|
||||
- A WebGL-capable canvas surface (the renderer is raw WebGL).
|
||||
- The GitHub Copilot app canvas / UI-extensions experiment enabled.
|
||||
|
||||
## Install
|
||||
|
||||
Drop this folder at `~/.copilot/extensions/backrooms-canvas/` for user scope, or in a repository at `.github/extensions/backrooms-canvas/` for project scope. Then install dependencies from inside the copied folder:
|
||||
|
||||
```sh
|
||||
# User scope
|
||||
cd ~/.copilot/extensions/backrooms-canvas
|
||||
|
||||
# Or project scope, from the repository root
|
||||
cd .github/extensions/backrooms-canvas
|
||||
|
||||
npm install
|
||||
```
|
||||
|
||||
Reload extensions in the GitHub Copilot app, then open the `backrooms-canvas` canvas. Click the view to capture the mouse and start walking.
|
||||
|
||||
The canvas accepts optional open inputs:
|
||||
|
||||
| Input | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `seed` | number | Maze seed. The same seed always rebuilds the same halls. `0` rolls a random seed. |
|
||||
| `materialPreset` | string | Wall material set: `classic`, `office`, `pool`, `concrete`, or `panel`. |
|
||||
| `monsterEnabled` | boolean | Whether something else walks the halls. |
|
||||
|
||||
## Controls
|
||||
|
||||
| Input | Action |
|
||||
| --- | --- |
|
||||
| `W` / `S` or `Up` / `Down` | Walk forward / back |
|
||||
| `A` / `D` | Strafe left / right |
|
||||
| `Left` / `Right` or `Q` / `E` | Turn |
|
||||
| `Shift` | Hurry |
|
||||
| Mouse (after clicking the view) | Look around |
|
||||
| `M` or `Esc` | Open the in-game menu |
|
||||
|
||||
The in-game menu has Resume, Relocate, Settings, and Help, plus live stats. Settings changed there persist in the canvas via `localStorage`, so your choices survive a reload.
|
||||
|
||||
## Agent actions
|
||||
|
||||
The agent drives the game and feeds the ghost-writer through three actions. They are the canvas equivalent of the original extension's `backviews_reportJob` tool and chat-session mirror.
|
||||
|
||||
- `report_job { status, tokens?, done? }` — report the current job step. The status text is scrawled on the walls and the token count drives the HUD counter. Call it when work starts, again on each new step, and once more with `done: true` when finished.
|
||||
- `ghost_write { text, tokens?, done? }` — ghost-write a block of text onto the wall ahead, character by character as it grows, like a streaming response. Send the growing text on each call, then once with `done: true` to settle it in place.
|
||||
- `relocate` — drop the wanderer into a fresh random seed, wiping any writing already on the walls.
|
||||
|
||||
To have the agent narrate itself automatically, reference these actions from a `.github/copilot-instructions.md` so it calls `report_job` on each step of a chat request.
|
||||
|
||||
## How the port works
|
||||
|
||||
The game itself is unchanged: `game/webview.js` is the same self-contained bundle the VSCode extension ships (WebGL renderer, maze engine, camcorder overlay, and wall-writing subsystem in one esbuild IIFE). The bundle was written to talk to a VSCode webview host through `acquireVsCodeApi()` and `window.postMessage`.
|
||||
|
||||
`game/index.html` shims that host:
|
||||
|
||||
- `acquireVsCodeApi()` is backed by `localStorage` for state (seed and player position) and settings persistence.
|
||||
- The canvas server (`extension.mjs`) runs a loopback HTTP server that serves the bundle and streams agent activity over Server-Sent Events at `/events`. The shim translates those events into the exact `jobStatus`, `chatSession`, and `relocate` messages the bundle already listens for.
|
||||
- Agent actions broadcast onto that SSE stream, so `report_job` writes on the walls and `ghost_write` streams text just as the VSCode chat mirror did.
|
||||
|
||||
This mirrors the [`arcade-canvas`](../arcade-canvas) architecture: a static frontend served from a loopback server, with the agent driving it through canvas actions.
|
||||
|
||||
## Credits
|
||||
|
||||
- Maze generation: [cmd-backedges](https://github.com/isocialPractice/cmd-backedges) by John Haugabook.
|
||||
- Original extension: [vscode-backviews](https://github.com/isocialPractice/vscode-backviews).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* Copilot canvas entry point for BackRooms.
|
||||
*
|
||||
* The game itself is the prebuilt browser bundle at game/webview.js (an
|
||||
* esbuild IIFE that already carries the WebGL renderer, the procedural maze
|
||||
* engine, and the whole first-person world). This file is only the host: a
|
||||
* tiny local HTTP server that serves that bundle plus its photo materials,
|
||||
* and a canvas registration that lets an agent drive the game through actions.
|
||||
*
|
||||
* The bundle was written for a VSCode webview and talks to its host through
|
||||
* `acquireVsCodeApi()` + `window.postMessage`. game/index.html shims that API
|
||||
* against this server's Server-Sent Events stream, so the same bundle runs
|
||||
* unchanged inside the canvas. The message shapes below match the bundle's
|
||||
* expectations exactly (see the original src/shared/settings.ts):
|
||||
*
|
||||
* host -> game : { type: 'config', settings }
|
||||
* { type: 'jobStatus', job } // CopilotJob
|
||||
* { type: 'chatSession', session } // ChatSessionSnapshot
|
||||
* { type: 'relocate' }
|
||||
* game -> host : { type: 'ready' } // handled in the shim
|
||||
* { type: 'updateSetting', key, value } // handled in the shim
|
||||
*/
|
||||
import { createReadStream } from "node:fs";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { CanvasError, createCanvas, joinSession } from "@github/copilot-sdk/extension";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const gameRoot = path.join(__dirname, "game");
|
||||
const materialsRoot = path.join(__dirname, "materials");
|
||||
const assetsRoot = path.join(__dirname, "assets");
|
||||
const indexPath = path.join(gameRoot, "index.html");
|
||||
|
||||
/**
|
||||
* Default settings, mirrored from the bundle's own DEFAULT_SETTINGS. The shim
|
||||
* merges any per-open overrides and the player's saved menu choices on top of
|
||||
* these before handing the game its first `config` message.
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
seed: 0,
|
||||
moveSpeed: 2.2,
|
||||
renderDistance: 14,
|
||||
cameraShake: true,
|
||||
filmGrain: true,
|
||||
vhsHud: true,
|
||||
furniture: true,
|
||||
wallpaperShifts: false,
|
||||
mouseLook: true,
|
||||
invertTurn: false,
|
||||
invertStrafe: false,
|
||||
invertForward: false,
|
||||
materialPreset: "classic",
|
||||
materialHueShift: 0,
|
||||
materialBrightness: 1,
|
||||
monsterEnabled: true,
|
||||
monsterSpeed: 2.6,
|
||||
monsterSpawnMin: 1,
|
||||
monsterSpawnMax: 5,
|
||||
monsterForm: "random",
|
||||
copilotGhostWriter: true,
|
||||
};
|
||||
|
||||
const MATERIAL_PRESETS = ["classic", "office", "pool", "concrete", "panel"];
|
||||
const MONSTER_FORMS = ["spider", "humanoid", "cloud", "random"];
|
||||
|
||||
/** Idle Copilot job; the walls stay quiet and the HUD counter fades out. */
|
||||
const IDLE_JOB = { working: false, status: "", tokens: 0 };
|
||||
|
||||
const servers = new Map();
|
||||
|
||||
function contentType(filePath) {
|
||||
switch (path.extname(filePath).toLowerCase()) {
|
||||
case ".html":
|
||||
return "text/html; charset=utf-8";
|
||||
case ".js":
|
||||
return "text/javascript; charset=utf-8";
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8";
|
||||
case ".json":
|
||||
return "application/json; charset=utf-8";
|
||||
case ".map":
|
||||
return "application/json; charset=utf-8";
|
||||
case ".png":
|
||||
return "image/png";
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "image/jpeg";
|
||||
case ".svg":
|
||||
return "image/svg+xml";
|
||||
case ".webp":
|
||||
return "image/webp";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolves a request path under a root, refusing anything that escapes it. */
|
||||
function resolveUnder(root, requestPath) {
|
||||
const resolved = path.resolve(root, `.${requestPath}`);
|
||||
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
||||
throw new CanvasError("invalid_path", "Requested path is outside the backrooms assets.");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function sendJson(res, value) {
|
||||
res.writeHead(200, {
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"cache-control": "no-store",
|
||||
});
|
||||
res.end(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function sendNotFound(res) {
|
||||
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
||||
res.end("Not found");
|
||||
}
|
||||
|
||||
function sendSse(res, event, data) {
|
||||
res.write(`event: ${event}\n`);
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
function broadcast(entry, event, data) {
|
||||
for (const client of entry.clients) {
|
||||
sendSse(client, event, data);
|
||||
}
|
||||
}
|
||||
|
||||
function randomSeed() {
|
||||
return Math.floor(Math.random() * 999_999) + 1;
|
||||
}
|
||||
|
||||
/** Coerces one enum-ish value, falling back to the default when unknown. */
|
||||
function pickEnum(value, allowed, fallback) {
|
||||
return typeof value === "string" && allowed.includes(value) ? value : fallback;
|
||||
}
|
||||
|
||||
/** Coerces a finite number into [min, max], or returns the fallback. */
|
||||
function clampNumber(value, min, max, fallback) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns arbitrary open-input into a partial settings override the shim can
|
||||
* merge over the defaults. Only the settings that make sense to preset from an
|
||||
* agent are honored; everything else stays on its default or saved value.
|
||||
*/
|
||||
function normalizeOverrides(input) {
|
||||
const overrides = {};
|
||||
if (!input || typeof input !== "object") {
|
||||
return overrides;
|
||||
}
|
||||
if (input.seed !== undefined) {
|
||||
const seed = Math.trunc(clampNumber(input.seed, 0, Number.MAX_SAFE_INTEGER, 0));
|
||||
overrides.seed = seed === 0 ? randomSeed() : seed;
|
||||
}
|
||||
if (input.materialPreset !== undefined) {
|
||||
overrides.materialPreset = pickEnum(input.materialPreset, MATERIAL_PRESETS, "classic");
|
||||
}
|
||||
if (input.monsterEnabled !== undefined) {
|
||||
overrides.monsterEnabled = input.monsterEnabled === true;
|
||||
}
|
||||
if (input.monsterForm !== undefined) {
|
||||
overrides.monsterForm = pickEnum(input.monsterForm, MONSTER_FORMS, "random");
|
||||
}
|
||||
if (input.copilotGhostWriter !== undefined) {
|
||||
overrides.copilotGhostWriter = input.copilotGhostWriter !== false;
|
||||
}
|
||||
return overrides;
|
||||
}
|
||||
|
||||
/** Coerces report_job input into a CopilotJob the game understands. */
|
||||
function normalizeJob(input) {
|
||||
if (!input || typeof input !== "object") {
|
||||
return { ...IDLE_JOB };
|
||||
}
|
||||
const done = input.done === true;
|
||||
return {
|
||||
working: !done,
|
||||
status: typeof input.status === "string" ? input.status.slice(0, 120) : "",
|
||||
tokens:
|
||||
typeof input.tokens === "number" && Number.isFinite(input.tokens)
|
||||
? Math.max(0, Math.floor(input.tokens))
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Coerces ghost_write input into a ChatSessionSnapshot. The game ghost-writes
|
||||
* `current` onto the wall ahead as it grows, and the token count drives the
|
||||
* HUD odometer. `done` settles the writing in place and stops the counter.
|
||||
*/
|
||||
function normalizeSession(input) {
|
||||
const text = typeof input?.text === "string" ? input.text : "";
|
||||
const done = input?.done === true;
|
||||
const tokens =
|
||||
typeof input?.tokens === "number" && Number.isFinite(input.tokens)
|
||||
? Math.max(0, Math.floor(input.tokens))
|
||||
: Math.ceil(text.length / 4);
|
||||
return { working: !done && text.length > 0, history: [], current: text, tokens };
|
||||
}
|
||||
|
||||
async function renderIndex(entry) {
|
||||
const html = await readFile(indexPath, "utf8");
|
||||
// Handed to the shim so it can build the game's first `config` message and
|
||||
// replay any job that is already running when the panel opens.
|
||||
const init = {
|
||||
defaults: DEFAULT_SETTINGS,
|
||||
overrides: entry.overrides,
|
||||
materials: {
|
||||
wallpaper: "/materials/wallpaper.jpg",
|
||||
ceiling: "/materials/ceiling.jpg",
|
||||
carpet: "/materials/carpet.jpg",
|
||||
},
|
||||
job: entry.job,
|
||||
session: entry.session,
|
||||
};
|
||||
return html.replace("__BACKROOMS_INIT__", JSON.stringify(init).replace(/</g, "\\u003c"));
|
||||
}
|
||||
|
||||
async function streamFile(res, filePath) {
|
||||
const fileStat = await stat(filePath).catch(() => undefined);
|
||||
if (!fileStat?.isFile()) {
|
||||
sendNotFound(res);
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, {
|
||||
"content-type": contentType(filePath),
|
||||
"cache-control": "no-cache",
|
||||
});
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("error", () => {
|
||||
if (!res.headersSent) {
|
||||
sendNotFound(res);
|
||||
} else {
|
||||
res.destroy();
|
||||
}
|
||||
});
|
||||
stream.pipe(res);
|
||||
}
|
||||
|
||||
async function handleRequest(entry, req, res) {
|
||||
const url = new URL(req.url ?? "/", entry.url);
|
||||
|
||||
if (url.pathname === "/events") {
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/event-stream; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
connection: "keep-alive",
|
||||
});
|
||||
entry.clients.add(res);
|
||||
// Catch a fresh (or reconnecting) client up to the live state.
|
||||
sendSse(res, "jobStatus", { job: entry.job });
|
||||
if (entry.session) {
|
||||
sendSse(res, "chatSession", { session: entry.session });
|
||||
}
|
||||
req.on("close", () => entry.clients.delete(res));
|
||||
return;
|
||||
}
|
||||
|
||||
// The shim reports explicit setting changes (menu edits, relocate): the
|
||||
// per-open override for that key stops applying, so a reload keeps the
|
||||
// player's choice instead of replaying the stale override.
|
||||
if (req.method === "DELETE" && url.pathname.startsWith("/override/")) {
|
||||
delete entry.overrides[decodeURIComponent(url.pathname.slice("/override/".length))];
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === "/favicon.ico") {
|
||||
await streamFile(res, path.join(assetsRoot, "icon.png"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (url.pathname === "/" || url.pathname === "/index.html") {
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/html; charset=utf-8",
|
||||
"cache-control": "no-cache",
|
||||
});
|
||||
res.end(await renderIndex(entry));
|
||||
return;
|
||||
}
|
||||
|
||||
const staticPath = url.pathname.startsWith("/materials/")
|
||||
? resolveUnder(materialsRoot, url.pathname.slice("/materials".length))
|
||||
: url.pathname.startsWith("/assets/")
|
||||
? resolveUnder(assetsRoot, url.pathname.slice("/assets".length))
|
||||
: resolveUnder(gameRoot, url.pathname);
|
||||
await streamFile(res, staticPath);
|
||||
} catch (error) {
|
||||
if (error instanceof CanvasError) {
|
||||
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
|
||||
res.end(error.message);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function startServer(instanceId, overrides) {
|
||||
const entry = {
|
||||
clients: new Set(),
|
||||
overrides,
|
||||
job: { ...IDLE_JOB },
|
||||
session: null,
|
||||
server: undefined,
|
||||
url: undefined,
|
||||
};
|
||||
const server = createServer((req, res) => {
|
||||
handleRequest(entry, req, res).catch((error) => {
|
||||
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
||||
res.end(error instanceof Error ? error.message : "Backrooms canvas server error");
|
||||
});
|
||||
});
|
||||
entry.server = server;
|
||||
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
const port = typeof address === "object" && address ? address.port : 0;
|
||||
entry.url = `http://127.0.0.1:${port}/`;
|
||||
servers.set(instanceId, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
function getOpenEntry(instanceId) {
|
||||
const entry = servers.get(instanceId);
|
||||
if (!entry) {
|
||||
throw new CanvasError("backrooms_not_open", "Open the Backrooms canvas before invoking this action.");
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
await joinSession({
|
||||
canvases: [
|
||||
createCanvas({
|
||||
id: "backrooms-canvas",
|
||||
displayName: "BackRooms",
|
||||
description:
|
||||
"An endless first-person backrooms to wander while agents work. The agent's status ghost-writes on the walls.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
seed: {
|
||||
type: "number",
|
||||
description: "Maze seed. The same seed always rebuilds the same halls. 0 rolls a random seed.",
|
||||
},
|
||||
materialPreset: {
|
||||
type: "string",
|
||||
enum: MATERIAL_PRESETS,
|
||||
description: "Wall material set to start with.",
|
||||
},
|
||||
monsterEnabled: {
|
||||
type: "boolean",
|
||||
description: "Whether something else walks the halls.",
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
name: "report_job",
|
||||
description:
|
||||
"Report your current job status to the backrooms. Call it when you start a chat request, again on each new step, and once more with done=true when finished. The status text is scrawled on the walls and the token count drives a camcorder-style HUD counter.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
status: {
|
||||
type: "string",
|
||||
description:
|
||||
"Short status text to scrawl on the walls (e.g. 'reading the codebase', 'rewriting the parser').",
|
||||
},
|
||||
tokens: {
|
||||
type: "number",
|
||||
description: "Approximate tokens consumed by the current job so far.",
|
||||
},
|
||||
done: {
|
||||
type: "boolean",
|
||||
description: "Set true when the job is finished; the walls stop updating and the counter fades out.",
|
||||
},
|
||||
},
|
||||
required: ["status"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: (ctx) => {
|
||||
const entry = getOpenEntry(ctx.instanceId);
|
||||
entry.job = normalizeJob(ctx.input);
|
||||
broadcast(entry, "jobStatus", { job: entry.job });
|
||||
return { job: entry.job };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ghost_write",
|
||||
description:
|
||||
"Ghost-write a block of text onto the walls ahead, character by character as it grows, like a streaming chat response. Call repeatedly with the full text so far, then once with done=true to settle it in place.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
text: {
|
||||
type: "string",
|
||||
description: "The full response text so far. Send the growing text on each call; the walls reveal the new characters.",
|
||||
},
|
||||
tokens: {
|
||||
type: "number",
|
||||
description: "Approximate tokens of the response so far. Defaults to text length / 4.",
|
||||
},
|
||||
done: {
|
||||
type: "boolean",
|
||||
description: "Set true when the response is complete; the writing settles onto the wall and the counter stops.",
|
||||
},
|
||||
},
|
||||
required: ["text"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
handler: (ctx) => {
|
||||
const entry = getOpenEntry(ctx.instanceId);
|
||||
entry.session = normalizeSession(ctx.input);
|
||||
broadcast(entry, "chatSession", { session: entry.session });
|
||||
return { working: entry.session.working, tokens: entry.session.tokens };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relocate",
|
||||
description: "Drop the wanderer into a fresh random seed, wiping any writing already on the walls.",
|
||||
handler: (ctx) => {
|
||||
const entry = getOpenEntry(ctx.instanceId);
|
||||
const seed = randomSeed();
|
||||
// The walls are wiped on relocate, so drop the job and
|
||||
// session snapshots too or a reload would replay them
|
||||
// onto the fresh maze.
|
||||
entry.job = { ...IDLE_JOB };
|
||||
entry.session = null;
|
||||
broadcast(entry, "jobStatus", { job: entry.job });
|
||||
broadcast(entry, "relocate", { seed });
|
||||
return { seed };
|
||||
},
|
||||
},
|
||||
],
|
||||
open: async (ctx) => {
|
||||
const overrides = normalizeOverrides(ctx.input);
|
||||
let entry = servers.get(ctx.instanceId);
|
||||
if (!entry) {
|
||||
entry = await startServer(ctx.instanceId, overrides);
|
||||
} else {
|
||||
entry.overrides = { ...entry.overrides, ...overrides };
|
||||
}
|
||||
return {
|
||||
title: "BackRooms",
|
||||
status: entry.job.working && entry.job.status ? entry.job.status : "Wandering",
|
||||
url: entry.url,
|
||||
};
|
||||
},
|
||||
onClose: async (ctx) => {
|
||||
const entry = servers.get(ctx.instanceId);
|
||||
if (!entry) return;
|
||||
servers.delete(ctx.instanceId);
|
||||
for (const client of entry.clients) {
|
||||
client.end();
|
||||
}
|
||||
await new Promise((resolve) => entry.server.close(() => resolve()));
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self';">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BackRooms</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #101010; }
|
||||
#app { position: relative; width: 100%; height: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
/*
|
||||
* Host shim. The game bundle (webview.js) was written for a VSCode webview:
|
||||
* it calls acquireVsCodeApi() for state + host messaging and reads its
|
||||
* material URIs off window.__BACKROOMS_MATERIALS__. This block stands in for
|
||||
* that host, backing state with localStorage and translating the canvas
|
||||
* server's Server-Sent Events into the window `message` events the bundle
|
||||
* already listens for. It MUST run before webview.js loads.
|
||||
*/
|
||||
(() => {
|
||||
const init = __BACKROOMS_INIT__;
|
||||
const SETTINGS_KEY = "backrooms.settings";
|
||||
const STATE_KEY = "backrooms.state";
|
||||
|
||||
// Material photo URIs the bundle patches over its procedural atlas.
|
||||
window.__BACKROOMS_MATERIALS__ = init.materials || {};
|
||||
|
||||
function readJson(key) {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, value) {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
// Storage disabled or full: settings/position just do not persist.
|
||||
}
|
||||
}
|
||||
|
||||
// Effective settings: bundle defaults, then the player's saved menu
|
||||
// choices, then any per-open overrides the agent requested win on top.
|
||||
function currentSettings() {
|
||||
const saved = readJson(SETTINGS_KEY) || {};
|
||||
return { ...init.defaults, ...saved, ...(init.overrides || {}) };
|
||||
}
|
||||
|
||||
function dispatch(message) {
|
||||
window.postMessage(message, "*");
|
||||
}
|
||||
|
||||
window.acquireVsCodeApi = () => ({
|
||||
postMessage(message) {
|
||||
if (!message || typeof message !== "object") {
|
||||
return;
|
||||
}
|
||||
if (message.type === "ready") {
|
||||
// Reply after the bundle has wired its own message listener.
|
||||
setTimeout(() => {
|
||||
dispatch({ type: "config", settings: currentSettings() });
|
||||
if (init.job) {
|
||||
dispatch({ type: "jobStatus", job: init.job });
|
||||
}
|
||||
if (init.session) {
|
||||
dispatch({ type: "chatSession", session: init.session });
|
||||
}
|
||||
}, 0);
|
||||
} else if (message.type === "updateSetting") {
|
||||
const saved = readJson(SETTINGS_KEY) || {};
|
||||
saved[message.key] = message.value;
|
||||
writeJson(SETTINGS_KEY, saved);
|
||||
// An explicit change beats a per-open override, now and after a
|
||||
// reload (the server replays its overrides into every reload).
|
||||
if (init.overrides && message.key in init.overrides) {
|
||||
delete init.overrides[message.key];
|
||||
fetch("/override/" + encodeURIComponent(message.key), { method: "DELETE" }).catch(() => {});
|
||||
}
|
||||
}
|
||||
},
|
||||
getState() {
|
||||
return readJson(STATE_KEY) || undefined;
|
||||
},
|
||||
setState(state) {
|
||||
writeJson(STATE_KEY, state);
|
||||
},
|
||||
});
|
||||
|
||||
// Live host -> game channel: the server pushes agent activity over SSE and
|
||||
// we forward it as the exact messages the bundle expects.
|
||||
try {
|
||||
const events = new EventSource("/events");
|
||||
events.addEventListener("jobStatus", (event) => {
|
||||
try {
|
||||
dispatch({ type: "jobStatus", job: JSON.parse(event.data).job });
|
||||
} catch {}
|
||||
});
|
||||
events.addEventListener("chatSession", (event) => {
|
||||
try {
|
||||
dispatch({ type: "chatSession", session: JSON.parse(event.data).session });
|
||||
} catch {}
|
||||
});
|
||||
events.addEventListener("relocate", (event) => {
|
||||
let seed;
|
||||
try {
|
||||
seed = JSON.parse(event.data).seed;
|
||||
} catch {}
|
||||
dispatch({ type: "relocate", seed });
|
||||
});
|
||||
events.addEventListener("reload", () => window.location.reload());
|
||||
} catch {
|
||||
// No EventSource: the game still runs, just without live agent activity.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="./webview.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 288 KiB |
+446
@@ -0,0 +1,446 @@
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "backrooms-canvas",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.71.tgz",
|
||||
"integrity": "sha512-F3axBi+sXSLYDJbxCBW36bM6MYKNC2rlyAf3Ivo/MjiHKKJW7j5AmaR1IRYS9Gt8r9mxOwWFM1cJFA+CuLaR8g==",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"copilot": "npm-loader.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/copilot-darwin-arm64": "1.0.71",
|
||||
"@github/copilot-darwin-x64": "1.0.71",
|
||||
"@github/copilot-linux-arm64": "1.0.71",
|
||||
"@github/copilot-linux-x64": "1.0.71",
|
||||
"@github/copilot-linuxmusl-arm64": "1.0.71",
|
||||
"@github/copilot-linuxmusl-x64": "1.0.71",
|
||||
"@github/copilot-win32-arm64": "1.0.71",
|
||||
"@github/copilot-win32-x64": "1.0.71"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-arm64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.71.tgz",
|
||||
"integrity": "sha512-mEWzyqbqRAWgyU7i2uuSRoVPx/TwaFQX0nZmw0bc30aJ0BnO7cy2kYQyCHw8ykmf/tfxT0xauZ6k0BOFmWizzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-x64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.71.tgz",
|
||||
"integrity": "sha512-Md9yEg406OBVBx3w4PeEj62TubulVLBcHleqmCoOoUmPgUxPZotUbrqz3rtbzADbXfrrD7JWvVsbd2UiNL194w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-arm64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.71.tgz",
|
||||
"integrity": "sha512-ykLJYOqBj3jRB5IJCDugLClAqbr7DmtTbUjlNY7+Jdq/n6i+d7xUQGclf1IWL5gnxbGQVAf+zkToD+sRM389Kg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-x64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.71.tgz",
|
||||
"integrity": "sha512-pC0FNHG+BBwZd6yZlM85kkAGN+uJhM6o+THi76N2GnnSxmw7+remb1mvYxdgRVbdCm+LBUIbCKRWJLuMwrfb6A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linuxmusl-arm64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.71.tgz",
|
||||
"integrity": "sha512-hBmDljFTjacxqZTasCEy43H8EIzuXB/hHEBBCMFjhB9J00nIxsO6Dh0woTifKpx7knTYZdpTjjca3D0pAoZlUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linuxmusl-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linuxmusl-x64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.71.tgz",
|
||||
"integrity": "sha512-CfTXU8pa5dxRz22xQzoi3TiG1PJo9+WR8PRDiPSdkIBSyPJ1NvX87DJmfXjTgeAfR+wkjt/p0keDCaBBVhNmUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linuxmusl-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.7.tgz",
|
||||
"integrity": "sha512-dgCFCPfxWUkrgclQbrm7WCFzTf5RnJHsK1Lqsc3KjPBbDLPutJT0qIGg3xJ0ZELLyX0icg3TOmVczhR4HdwHxw==",
|
||||
"dependencies": {
|
||||
"@github/copilot": "^1.0.71",
|
||||
"koffi": "^3.1.0",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-arm64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.71.tgz",
|
||||
"integrity": "sha512-+HI1DokixXhHUahj06Fw67ZAigBuXKC58BFma4UJOGrQsDgwOSbqeTQHCw6vuymzjKlg3sactfsCUTaefkjscQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-arm64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-x64": {
|
||||
"version": "1.0.71",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.71.tgz",
|
||||
"integrity": "sha512-02kXOBd9CwBbCaztuf71WYWn+uGapCuiaasomN4tcMH3HBVZ4gi3J0ZUoRcgcS80xh81uQyeBHbnUKzb/RE/9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-x64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-darwin-arm64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-arm64/-/koffi-darwin-arm64-3.1.1.tgz",
|
||||
"integrity": "sha512-+Dl0zQDh1Wb55AWOn9hp7K30qgkODvrvN+ZNkFOh81Q0oFX/rpJQtocgjAuYk2zFAcajSeVDumkcHMPwnKSXzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-darwin-x64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-darwin-x64/-/koffi-darwin-x64-3.1.1.tgz",
|
||||
"integrity": "sha512-cDFAKn1qdZBFLrp7dAc9QUDw3l4xAhTJbOdPWWb0LxssVicUdHcRCLZGrDsmPW2tpH6LGNNeLgqRpAoD2Mo8iA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-freebsd-arm64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-arm64/-/koffi-freebsd-arm64-3.1.1.tgz",
|
||||
"integrity": "sha512-zaP7FJISI/scQW9Wa5QicY3a09WmtKBWSbmC+5nfCqPzwWe7Hx2so74Er7mPsDfCiMMR0Ya+evKbJQDkfyXicg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-freebsd-ia32": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-ia32/-/koffi-freebsd-ia32-3.1.1.tgz",
|
||||
"integrity": "sha512-7GejVb688TLM8rbjfc0oezJrATxZc0dn801xWEDJekN2DgmRXu7HquGqWQ6z3NeSq7ZxEggz4T3xtlbCysQapA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-freebsd-x64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-freebsd-x64/-/koffi-freebsd-x64-3.1.1.tgz",
|
||||
"integrity": "sha512-XLiCFP9OFCyOoGTjAimtDKLhzhfo34WcP1ShVWxRzNCWDGjfz8BYjwd69cp/cDSUXZbxamqs4+/6vmkePq9wxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-linux-arm64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-linux-arm64/-/koffi-linux-arm64-3.1.1.tgz",
|
||||
"integrity": "sha512-HA9xINK7G4dRAkpfnBWD9VfuyIBgW1SuK+KPHjksUwRMOnhgqP8J/JqgrAzdzcDiefGBkqEacIP776OUwz7knQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-linux-ia32": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-linux-ia32/-/koffi-linux-ia32-3.1.1.tgz",
|
||||
"integrity": "sha512-jG7IFytmP8K5Qtbx0ro0ZeuX3JjSsLxmYhq+nmXDdrtOAlxIsWGynuiDLS6Jk3vOchVii2m6Y2f/L3GLG2fG5A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-linux-loong64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-linux-loong64/-/koffi-linux-loong64-3.1.1.tgz",
|
||||
"integrity": "sha512-CIsT1cNnih8FuU52Me/IVlJBpH28SQfoDeYPctJswgJzaARktusF7m4MUbtR1PBDjuquCVM4/vFyNdOzfPonvA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-linux-riscv64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-linux-riscv64/-/koffi-linux-riscv64-3.1.1.tgz",
|
||||
"integrity": "sha512-9D6RmqeKsSvs3U6jILJU9PcAjMwKKyn7yLxNBb5k6z9PCoUoGJ3/BrhXAX0qjrLLwEiIpP/hS/40RuXvH8Lc3Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-linux-x64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-linux-x64/-/koffi-linux-x64-3.1.1.tgz",
|
||||
"integrity": "sha512-pyTcX5fePeYbt7TZAwRby69wdlRx3PT+g15ra5IYdat/Pgh3qAKEYeZ+uu7WpPGOy43p/oSRqqZoa2kORzozlA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-openbsd-ia32": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-ia32/-/koffi-openbsd-ia32-3.1.1.tgz",
|
||||
"integrity": "sha512-iPnPzvG2HOfdzaiG1drdkt86sAqmTPDv9mAf+5gL7mRzkeeQC88EVGboRy7eXwdXn7R+v0ntA3iQxdHrBn6yXw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-openbsd-x64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-openbsd-x64/-/koffi-openbsd-x64-3.1.1.tgz",
|
||||
"integrity": "sha512-/Xqc3R0SVoMCYjMPZnJ9bULtRo364+dKmnQhfDrI83tSpxUHRw7HRNf12vBeL+hPgKxSBjtMpWfQ/ZIyVyLFag==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-win32-arm64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-win32-arm64/-/koffi-win32-arm64-3.1.1.tgz",
|
||||
"integrity": "sha512-JhqHauEwQvdcWUERxrV5HH/DT9W7hY1A1eU6/o8tB+yck+D3kt5elpRDBt9KjpW6h+vHPy3V0sjDvO0CXyabTA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-win32-ia32": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-win32-ia32/-/koffi-win32-ia32-3.1.1.tgz",
|
||||
"integrity": "sha512-ZRuyYmlGS/rCc966qqs0qREXDW4FRdul7rDF1VgSWHbVmdc196PUgUT+blq/GjZgTwqzeEXtMRgM+cU8krHjvA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/@koromix/koffi-win32-x64": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@koromix/koffi-win32-x64/-/koffi-win32-x64-3.1.1.tgz",
|
||||
"integrity": "sha512-KqHPmvj6QILhNyI/To8QSihHsijeVGIYYPBOUnXEpcnH2LuLbargY4Hd6dDeTN3Z90uUUxN+1FWz1UnhVzFOiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/koffi": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-3.1.1.tgz",
|
||||
"integrity": "sha512-mRX6AMeeKCxSOeOopqAcLAl5jcNvge7NAG8l7rF/8gGJATI0tdHFYjteIdE0mGOtWdsrJOij+PjnP8Q9c1gwgA==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"url": "https://liberapay.com/Koromix"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@koromix/koffi-darwin-arm64": "3.1.1",
|
||||
"@koromix/koffi-darwin-x64": "3.1.1",
|
||||
"@koromix/koffi-freebsd-arm64": "3.1.1",
|
||||
"@koromix/koffi-freebsd-ia32": "3.1.1",
|
||||
"@koromix/koffi-freebsd-x64": "3.1.1",
|
||||
"@koromix/koffi-linux-arm64": "3.1.1",
|
||||
"@koromix/koffi-linux-ia32": "3.1.1",
|
||||
"@koromix/koffi-linux-loong64": "3.1.1",
|
||||
"@koromix/koffi-linux-riscv64": "3.1.1",
|
||||
"@koromix/koffi-linux-x64": "3.1.1",
|
||||
"@koromix/koffi-openbsd-ia32": "3.1.1",
|
||||
"@koromix/koffi-openbsd-x64": "3.1.1",
|
||||
"@koromix/koffi-win32-arm64": "3.1.1",
|
||||
"@koromix/koffi-win32-ia32": "3.1.1",
|
||||
"@koromix/koffi-win32-x64": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
|
||||
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "backrooms-canvas",
|
||||
"version": "1.0.0",
|
||||
"main": "extension.mjs",
|
||||
"author": "John Haugabook",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@github/copilot-sdk": "latest"
|
||||
},
|
||||
"description": "Wander an endless first-person backrooms in a Copilot canvas while agents work; their status ghost-writes on the walls.",
|
||||
"keywords": [
|
||||
"backrooms",
|
||||
"copilot-canvas",
|
||||
"interactive-canvas",
|
||||
"first-person",
|
||||
"procedural-generation",
|
||||
"session-breaks"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user