Home / Docs
Making a workspace
A workspace describes one project: the environments it runs in, and how their addresses relate. You write it once as JSON in the settings page, and the popup uses it to turn whatever page you are on into the same page everywhere else.
Would you rather have an assistant write it?
The same rules exist as a specification you can paste into Claude,
ChatGPT or Copilot along with a few real addresses.
Everything below was checked against the shipped code.
The smallest one that works
{
"workspaceName": "My Project",
"environments": [
{
"name": "Dev",
"links": [
{ "name": "Dev site", "url": "https://dev.example.com/{{path}}" }
]
},
{
"name": "Live",
"links": [
{ "name": "Live site", "url": "https://www.example.com/{{path}}" }
]
}
]
}
One workspace is one object. The settings page keeps the list for you: every workspace you create is a card of its own, and opening one puts that object, on its own, in the editor.
Ask an assistant for several projects at once and it hands back a JSON array instead. Paste that straight into a new workspace and you get one card per entry, in the order they arrived. Pasting an array over a workspace you already have is the one case that will not work, since there is no telling which card the extra entries were meant to become.
Open https://www.example.com/products/pump-42 and the
popup offers https://dev.example.com/products/pump-42.
It works in both directions, because the extension matches the page
you are on against every link in the workspace.
How it finds your workspace
The extension takes the hostname of the page you are on and looks for a link whose hostname appears inside it. First match wins, and that decides the workspace.
Three consequences worth knowing.
A page can only be matched if some link mentions its host, so a ticket system you want to jump from needs a link of its own.
The match is a substring test, so a link for
example.com also matches
example.com.something-else.org. Prefer full hostnames.
Do not let one host nest inside another
If dev is dev.example.com and a branch deployment is
mr-3282.dev.example.com, the second contains the
first. On the branch deployment the extension matches the
dev environment, because that link comes earlier and its
hostname is present. Path extraction then runs against the wrong
environment and comes out empty, so every link loses the page you
were on. Give branch deployments a host of their own —
mr-3282.branch.example.com — and the problem goes
away. First match wins here, not closest match.
The two built-in placeholders
{{path}} is whatever follows the base address, after
the locale is taken out. {{language}} is the locale
segment, when the address has one.
address https://www.example.com/de-de/products/pump-42
language de-de
path products/pump-42
A query string stays with the path, so ?serial=ETB1996
survives the trip to another environment.
A link with no placeholders at all gets the locale and the path
added to the end — but only when it is nothing more than a host.
That is what makes https://dev.example.com a usable
link. A link that carries its own path or query is left exactly as
you wrote it, because that is an action rather than a page: a
cache-clear endpoint, a webhook, a health check.
Left to guess, locale detection recognises de,
de-de, en-global and
demo-de-de — and any other two-letter
segment besides, so an address like /ui/settings gives
you language: "ui" and path: "settings".
The guess is also lowercase-only. de-DE is the
canonical spelling, and the guess misses it completely: the locale
quietly drops out of every link built from that tab, with no error
anywhere.
Tell it which locales you use
One line at the top of the workspace closes both holes. Detection becomes a lookup against your list, matched whatever the casing:
{
"workspaceName": "Example",
"schemaVersion": 2,
"languages": ["de-DE", "en-GB", "en-global"],
"environments": [ … ]
}
Worth doing for any project with locales in its paths. If your paths
start with two-letter segments that are not locales, leave
{{language}} out of those templates instead.
Filling in a missing locale
Some pages carry no locale — a ticket page, for instance. Give a link a fallback:
{
"name": "Dev site",
"url": "https://dev.example.com/{{language}}/{{path}}",
"defaultLanguage": "de-de"
}
defaultPath works the same way for the path.
Your own values
Anything else the address might contain gets declared as a variable: a ticket number, a merge request number, a build id. You say what it is called and where to find it.
"variables": [
{
"name": "ticketNumber",
"label": "Ticket number",
"extractFrom": [{ "urlPattern": "PROJ-(\\d+)" }],
"promptIfMissing": true
}
]
Then use it in a link as {{ticketNumber}}:
{ "name": "Ticket", "url": "https://example.atlassian.net/browse/PROJ-{{ticketNumber}}" }
The pattern is a regular expression run against the address of the
page you are on.
The part in brackets is the value. Everything else
just says where to look. Remember that JSON needs the backslash
doubled, so \d is written \\d.
You can give several patterns and they are tried in order:
"extractFrom": [
{ "urlPattern": "/-/merge_requests/(\\d+)" },
{ "urlPattern": "^https?://mr-(\\d+)\\." }
]
The first says “when I am on the merge request page, take the number from there”. The second says “when I am on the deployment itself, take it back out of the hostname”.
A link that needs a value it does not have
It is left out. No link with a hole in it, and nothing that looks right and goes nowhere. A link you expect and do not see is most often waiting on a value, and the diagnostics panel names which one.
Where a value can come from
Four places, and they do not carry equal weight:
| Source | Notes |
|---|---|
| The address of the page you are on | Always wins. An address cannot be out of date. |
| The page itself |
Needs domPattern,
see below
|
| You typing it |
Set promptIfMissing and the popup offers a box
|
| Memory |
Set remember and it keeps the last one it found
|
What you type is cleaned up through the same patterns, so pasting a
whole merge request URL, or typing PROJ-42395 when only
the number is wanted, both work.
A remembered value is tied to the ticket it was found with, and offered again only on a page naming that ticket. On a page with no ticket — a board listing dozens of them, for instance — nothing is offered. A missing link beats a wrong one.
Reading a value off the page
Some numbers live in a page and never in its address. A merge request number sits in Jira's development panel, and no part of the ticket address contains it.
{
"domPattern": "/-/merge_requests/(\\d+)",
"scopeSelector": "[data-testid*='development-context-panel']",
"hoverSelector": "button",
"requireInScope": "PROJ-{{ticketNumber}}"
}
Read it as: look at links inside the development panel, hover its buttons first — because the panel builds itself only once hovered — and only trust a panel whose contents mention this ticket.
That last line is not decoration. A board view renders one panel per card, and without it the extension hands back a neighbouring ticket's number. If more than one distinct value turns up, it takes none of them.
This is the only feature that reads a page rather than an address. It runs on the click that opens the popup, on that tab alone, and sends nothing anywhere.
Keeping a group off the wrong pages
One project is one workspace, however many hosts it has. That
includes the ones whose addresses look nothing like the website's: a
media host, an API, a ticket system. The catch is that every link in
the matched workspace gets built, whether or not it fits the page
you are on. A media host sitting beside a website on nothing but
{{path}} offers two dead addresses on every page:
on https://www.example.com/de-de/products/pump-42
https://media.example.com/products/pump-42 ← nothing is there
https://media-stage.example.com/products/pump-42 ← nor here
onlyOn says where a group belongs. It is a pattern
tested against the whole address, and it goes on an environment, on
a single link, or on both:
{
"name": "Media",
"onlyOn": "^https?://media(?:-stage)?\\.example\\.com/",
"links": [
{ "name": "Live", "url": "https://media.example.com/{{path}}" },
{ "name": "Stage", "url": "https://media-stage.example.com/{{path}}" }
]
}
Now the media group appears on media pages and nowhere else, and the
website group the other way round. A link with no
onlyOn appears everywhere, which is what every link you
have written so far says.
The pattern only decides whether links get built. It never changes which workspace or environment your address matched, so adding one cannot move where a page resolves to. Unlike the patterns further up, it needs no brackets: it answers yes or no rather than handing back a value.
Trying an address before you save
The editor's sidebar has a Try an address box. Paste one page address into it and it tells you what this workspace would do with that page: the environment it matched, the locale, the path, every value it worked out, every link you would get, and every link you would not, with the reason each was held back.
It reads what is in the editor rather than what is saved, so you can check a workspace someone else wrote, or one an assistant gave you, before committing to it. Nothing is saved by trying an address.
Two things it cannot do. It tries only the workspace you have open, while the popup checks all of them and uses the first whose host matches your address. And it never reads a page, so a link waiting on a value that comes from the page itself shows as held back there and still works in the popup.
When a link is missing
Open the settings page with ?debug=1 on the end of the
address. The popup then shows what it worked out: the workspace it
matched, every value and where each came from, which links were held
back and what each was waiting for, and what happened on any attempt
to read the page. ?debug=0 switches it off.
Start there rather than guessing. It answers most questions on sight.
How much room you have
Workspaces are stored compressed, so a lot fits. The settings page shows a quiet grey line once the saved size passes four fifths of what Chrome allows for settings it syncs between computers, and different wording once past it. A save that Chrome refuses shows an error rather than a success message, so a workspace you appear to have saved is saved.
If you see that line, the usual cause is many large workspaces rather than one. Splitting a project into two workspaces does not help, since they share the same allowance.
One rough edge
Renaming a project means editing the patterns. A ticket prefix appears written out in each pattern that uses it, rather than being declared once. That is deliberate — a pattern you can read cannot go wrong in a way you cannot see — but it does mean two places to change.
A worked example
A project with three environments, a branch deployment keyed by merge request, and a link back to the ticket:
{
"workspaceName": "Example",
"schemaVersion": 2,
"variables": [
{
"name": "ticketNumber",
"label": "Ticket number",
"extractFrom": [{ "urlPattern": "PROJ-(\\d+)" }],
"promptIfMissing": true
},
{
"name": "mrNumber",
"label": "Merge request number",
"extractFrom": [
{ "urlPattern": "/-/merge_requests/(\\d+)" },
{ "urlPattern": "^https?://mr-(\\d+)\\." },
{
"domPattern": "/-/merge_requests/(\\d+)",
"scopeSelector": "[data-testid*='development-context-panel']",
"hoverSelector": "button",
"requireInScope": "PROJ-{{ticketNumber}}"
}
],
"remember": true,
"promptIfMissing": true
}
],
"environments": [
{
"name": "Dev",
"links": [
{ "name": "Dev", "url": "https://dev.example.com/{{language}}/{{path}}", "defaultLanguage": "de-de" }
]
},
{
"name": "Live",
"links": [
{ "name": "Live", "url": "https://www.example.com/{{language}}/{{path}}", "defaultLanguage": "de-de" }
]
},
{
"name": "Branch",
"links": [
{ "name": "Branch deployment", "url": "https://mr-{{mrNumber}}.branch.example.com/{{language}}/{{path}}", "defaultLanguage": "de-de" },
{ "name": "Merge request", "url": "https://gitlab.example.com/team/repo/-/merge_requests/{{mrNumber}}" },
{ "name": "Ticket", "url": "https://example.atlassian.net/browse/PROJ-{{ticketNumber}}" }
]
}
]
}
Standing on the ticket, the extension reads the merge request number off the page and offers the branch deployment. Standing on the branch deployment, it reads the number back out of the hostname, so the merge request link works, while the ticket link is held back because no ticket number appears in that address. Standing on the live site, it offers dev with the same page and locale.
The same rules, written as a specification for an assistant →