Skip to content
Rooms / Write a room

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.

{
"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}"
}
}
  1. Put it at ~/.config/squad-chat/rooms/<id>/room.json. The folder’s name must be the room’s id.
  2. Run /reload-plugins (or restart Claude Code), then /chat rooms +<id> and /chat <id>.
  3. If it doesn’t show, the reason is in the debug log (claude --debug), on a line that starts squad-chat: room … skipped.
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" }).
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.

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 from vars, 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 under key (a number becomes value). order keeps rows in the order a var lists them.
  • fields: for each row, a value by path (holes filled from vars too) and a format: text, number (digits for fixed decimals), compact (185.8M), percent, signed, signed-percent, date or age (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’s colorFrom.
  • values: the same, for single values at the top of the answer (base, date).

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.

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" }.

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.

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):

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), tag and sub (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). area and bars take a list of numbers, orbit, drop and progress a percent, battery a percent with flag for charging, thermometer degrees Celsius, weather an icon from open-meteo, equalizer a flag for playing, icon one of branch, pr, stack, flame, chip, net, spark or clock, and dot nothing.
  • Color: color, or colorFrom, a path to one of the color names the data picks (the providers give tones for this, such as sys.cpu.tone). A value takes the pill’s color when that is amber, red, rose or leaf.
  • when shows the pill only while that path has data. each repeats it for the first max items of a list (up to 6), which its paths reach as it.
  • 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.

  1. Add it as rooms/<id>/room.json in a fork of squad-chat.
  2. Run node rooms/build-index.mjs. It checks the room as the bridge will, and adds it to rooms/index.json.
  3. Open a pull request. CI runs the same check, and a review looks at what the room reaches. Once it’s merged, /chat store lists it.