Write a room
A room is one JSON file. How to write one, try it, and put it in the store.
A room is one file, room.json: what it’s called, which of squad-chat’s providers feed it, the hosts they may reach, its settings, and how its cards look. There’s no code in it, and there can’t be. That’s what lets anyone install a room without trusting whoever wrote it. If what you want needs a provider squad-chat doesn’t have, open an issue or a pull request for the provider itself.
A first room
Section titled “A first room”{ "schema": 1, "id": "rust-blog", "version": "1.0.0", "minSquadChat": "0.15.0", "name": "Rust Blog", "icon": "⚙", "color": "sand", "description": "New posts from the Rust blog.", "author": "you", "permissions": { "hosts": ["blog.rust-lang.org"] }, "settings": { "feeds": { "type": "list", "label": "Feeds", "item": "url", "max": 4, "default": ["https://blog.rust-lang.org/feed.xml"] } }, "providers": [ { "id": "feeds", "type": "rss", "params": { "feeds": "$settings.feeds" }, "interval": { "visible": "30m", "background": "2h" } } ], "layout": { "cards": [ { "title": "POSTS", "meta": "{feeds.count}", "body": { "type": "list", "items": "feeds.items", "title": "title", "preview": "meta", "copy": "url", "share": "share" } } ], "band": "{feeds.items.0.title}" }}Try it
Section titled “Try it”- Put it at
~/.config/squad-chat/rooms/<id>/room.json. The folder’s name must be the room’s id. - Run
/reload-plugins(or restart Claude Code), then/chat rooms +<id>and/chat <id>. - If it doesn’t show, the reason is in the debug log (
claude --debug), on a line that startssquad-chat: room … skipped.
The fields
Section titled “The fields”| Field | What it is |
|---|---|
schema |
Always 1. |
id |
2-24 lowercase letters, digits or -, starting with a letter. Not one squad-chat ships. |
version |
Like 1.0.0. Raise it with each change, so /chat update offers it. |
minSquadChat |
The oldest squad-chat it works with: 0.15.0 or newer, the first with the store. |
name, icon, color |
Up to 20 characters, one character, and one of sky, leaf, lilac, amber, coral, rose, teal, sand, red. |
description, author |
What it is, in up to 200 characters, and who wrote it. |
permissions.hosts |
Every host its providers may reach, up to 10. A provider can’t reach anything else, and installing shows this list. |
settings |
Up to 8 settings, changed with /chat set <room> <setting> <value>: list ("item": "url" for links, which must stay on the room’s hosts, "add": true to take what’s typed as more of it, and min and max for how many it holds), enum (values), string, bool or int (min, max), each with a default. |
providers |
Up to 4: an id (used in the layout’s paths), a type, params (a setting as "$settings.<key>"), and an interval (visible while the room is on show, background otherwise, at least 1s). |
layout |
cards (1-6), and optionally inline, band, pills, snapshot, hint, placeholder, keys and add (a list setting that what’s typed in the room’s box goes into, or { "action", "field" }). |
Providers
Section titled “Providers”| Type | Params | What its data holds |
|---|---|---|
http-json |
url, vars, rows, fields, values (see Any JSON API) |
rows[] with each field formatted, the values, count, updated |
rss |
feeds: feed URLs on the room’s hosts |
items[] (title, url, source, meta, share), count, failed |
hn |
list (top, best, new, off), count |
items[] as rss with score, comments and age, count, list |
open-meteo |
cities (each new one looked up first, and refused if there’s no such place), units (metric or imperial) |
cities[] (name, place (with the country code), query (as the setting has it), icon, desc, tempText, range, rain, rainText, rainNext (the next 8 hours’ chances), aqi, aqiText, aqiTone, days[], …), first, days[], updated |
quotes |
watchlist, colors (market, red-up, green-up), move |
quotes[] (symbol, name, priceText, pctText, arrow, color, spark, line (the day’s prices), volText, when), markets, statusText, statusTone, band, note, updated |
local-list |
max |
items[] (id, name, lang, body), count, latest (name, lang) |
sysinfo |
alerts, cpu_temp, memory |
cpu (with hist, tone, temp), mem (with tone), gpu, net, disk, battery (with charging, tone), band, … (see the Monitor room’s manifest) |
player |
stations (https, on the room’s hosts), volume |
now, playing, entries[], band, share (see the Lo-fi room’s manifest) |
Each provider can reach only the hosts it knows about, and only where the manifest names them too. rss and http-json read whatever they’re pointed at, so for them the manifest’s list alone decides. The rooms squad-chat ships, in plugins/squad-chat/rooms/, are full examples of each.
Any JSON API
Section titled “Any JSON API”http-json reads one https URL on the room’s hosts and makes its answer fit the widgets, with no code. The Crypto room in the store reads CoinGecko like this:
"params": { "url": "https://api.coingecko.com/api/v3/simple/price?ids={coins}&vs_currencies={currency}&include_24hr_change=true", "vars": { "coins": "$settings.coins", "currency": "$settings.currency" }, "rows": { "from": "", "key": "id", "order": "coins" }, "fields": { "price": { "path": "{currency}", "format": "number" }, "change": { "path": "{currency}_24h_change", "format": "signed-percent", "digits": 2 } }}url:{name}holes are filled fromvars, a list joined with commas, each part URL-encoded. A var is usually a setting.rows: where the list is (from, a path, empty for the top), as an array or as an object of objects. An object becomes one row per key, its key underkey(a number becomesvalue).orderkeeps rows in the order a var lists them.fields: for each row, a value bypath(holes filled fromvarstoo) and aformat:text,number(digitsfor fixed decimals),compact(185.8M),percent,signed,signed-percent,dateorage(3h). Each gives<name>to show and<name>Value, the number. A signed one adds<name>Arrow(▲ ▼ or – when it rounds to nothing) and<name>Color, green for up ("up": "red"for red), for a table column’scolorFrom.values: the same, for single values at the top of the answer (base,date).
Alerts
Section titled “Alerts”A room can toast when something crosses a line, once until it crosses back, and never during do not disturb:
"alerts": [ { "rows": "c.rows", "field": "changeValue", "beyond": "$settings.move", "text": "₿ {id} {changeArrow} {change}", "id": "{id}-{changeArrow}" }]rows and field watch each row, or value watches one number. above, below or beyond (either way) is the line: a number, or a setting, and a setting of 0 turns the alert off (a 0 written in the manifest is just zero). text and id are templates filled from the row, and the id decides what counts as the same alert.
Widgets
Section titled “Widgets”A card has a title, an optional meta, badge (a few words before the meta, colored by the path badgeTone) and when (show the card only while that path has data), a body: one widget, or a list of up to 6, and optionally a desktop body.
| Widget | Fields |
|---|---|
list |
items (a path to a list), title, and optionally mark (a short field shown before the title, as Lo-fi’s ♫), tag, preview, copy (⧉), share (⇪), act (a button on each row: label, action, field), max, empty |
table |
items, columns (each a field, and optionally label, width, grow, right, color or colorFrom), drop (a ✕ on each row that takes it out of a list setting: setting, field), pick (a row pressed is kept in a string setting by its field, and shown at the path as, as Weather’s cities pick wx.first), max, empty |
tiles |
tiles: up to 6, each a value and sub template |
meter |
label, value (a path to a percent), right, color |
text |
text, a template. A line whose holes all came out empty is left out. |
buttons |
buttons: up to 8, each a label and one of its provider’s actions |
A table column is width cells wide (12 if unset). The one marked grow takes the space left, or else the last column when it has no width.
Paths are plain dots into the providers’ data, starting with the provider’s id: feeds.items, wx.first.name. Templates put paths in braces: "{feeds.count} posts". keys maps single keys typed in the room to actions: { "p": "pause" }.
Cards on the desktop
Section titled “Cards on the desktop”A card’s desktop is a body the desktop app draws in place of body. The terminal keeps body. It takes the widgets above and these, drawn only there:
| Widget | Fields |
|---|---|
stat |
value (a path), right, color: a number set large |
bar |
value (a percent), color |
chart |
values (a list of numbers), values2, color, color2, max, caption |
grid |
items, title, and optionally sub, value, change, colorFrom, line, icon, note, across, pick, pickTitle, drop, hint, empty: items as tiles |
forecast |
values (a line over the hours), bars, marks, caption, color, color2 |
strip |
items, title, and optionally icon, value, sub, note, lit: items as columns, the lit one outlined |
sun |
rise, set, value (0-100 through the day), sub, note: an arc with the sun on it |
media |
title, sub, progress, elapsed, total, live, playing, empty: what’s playing |
A list takes more fields there: code (the preview as a code block), score (a story as Hacker News shows it), group (rows under a chip per value), open (a link to open), lit with pause (the row playing), and remove (a ✕ that runs an action). buttons takes split and level to put a level between them (“− 60% +”). The Weather, Lo-fi, Tech News, Stocks, Monitor and Snippets rooms in plugins/squad-chat/rooms/ each have a desktop body to copy from.
The band on the desktop
Section titled “The band on the desktop”In the terminal the band is the band line. In the desktop app it can be a row of pills, each a small drawing with a few words and a card on hover. Here is every room’s band as the app draws it (the figures are made up, hover a pill to light it):
◔ Usage the context as weather with a bar per prompt, the 5-hour and 7-day windows as orbits with a moon for the time gone, spend, cache, subagents
past the moon, the arc turns warm: using faster than time passes
◆ Chat who's online, what's new, the latest message, or who's typing
⎇ Git the branch, ahead and behind, changes, and the PR's checks as a ring
the ring: green passed, amber running, red failed
⟡ Agents a dot per Claude session on this computer, busy ones breathing, and one per subagent
▦ Monitor CPU and its trend, memory, network, temperature, battery, each turning amber or red when it needs you
☀ Weather the sky and temperature, a drop filled with the chance of rain, the next hours, the air
$ Stocks whether the market is open, and each symbol with its day's line
✦ Tech News the top story and its score
⌘ Snippets how many, and the latest saved
♫ Lo-fi an equalizer that dances while it plays and rests when paused, the title, where it is
pills lists up to 6:
"pills": [ { "draw": { "type": "icon", "icon": "chip" }, "colorFrom": "sys.cpu.tone", "value": "{sys.cpu.pct}%", "tip": ["CPU {sys.cpu.pct}%", "{sys.cpu.meta}"] }, { "each": "q.quotes", "max": 4, "draw": { "type": "area", "value": "it.line" }, "colorFrom": "it.color", "label": "{it.symbol}", "value": "{it.arrow}{it.pctText}" }]- Words:
label(dim, before the value),value(bold),msg(a long text that gives way when the band is narrow),tagandsub(dim, after). Each is a template, and one whose holes all came out empty is left out. draw: one of the drawings squad-chat ships, and where its number comes from (value, a path).areaandbarstake a list of numbers,orbit,dropandprogressa percent,batterya percent withflagfor charging,thermometerdegrees Celsius,weatheran icon fromopen-meteo,equalizeraflagfor playing,iconone ofbranch,pr,stack,flame,chip,net,sparkorclock, anddotnothing.- Color:
color, orcolorFrom, a path to one of the color names the data picks (the providers give tones for this, such assys.cpu.tone). A value takes the pill’s color when that isamber,red,roseorleaf. whenshows the pill only while that path has data.eachrepeats it for the firstmaxitems of a list (up to 6), which its paths reach asit.tip: up to 6 lines for the card on hover, the first one in the pill’s color.
A room without pills shows its band line in the desktop’s capsule too.
Put it in the store
Section titled “Put it in the store”- Add it as
rooms/<id>/room.jsonin a fork of squad-chat. - Run
node rooms/build-index.mjs. It checks the room as the bridge will, and adds it torooms/index.json. - Open a pull request. CI runs the same check, and a review looks at what the room reaches. Once it’s merged,
/chat storelists it.