Skip to content

feat(notify,theia): a notification can land in the IDE, not only in a chat - #83

Merged
AndrejK666 merged 1 commit into
constructorfabric:mainfrom
AndrejK666:AndrejK666/notify-the-ide
Sep 9, 2026
Merged

feat(notify,theia): a notification can land in the IDE, not only in a chat#83
AndrejK666 merged 1 commit into
constructorfabric:mainfrom
AndrejK666:AndrejK666/notify-the-ide

Conversation

@AndrejK666

Copy link
Copy Markdown
Contributor

Studio knows things the person working does not — an import finished, a publish
was rejected, a schedule fired — and until now the only way to tell them was a
chat message they may not be reading. This puts it where they already are.

POST /studio-notify/v1/messages
{
  "workspace_id": "",          # instead of connection_id
  "level": "warn",              # info (default) | warn | error
  "title": "The repository import failed",
  "text": "GitHub answered 401 — the token was rotated.",
  "link": "http://localhost:8080/projects/14/sources"
}

Same queue, same run, same retries: the editor is a second kind of
destination, not a second notification system. title is the headline the
IDE leads with, text the line beneath it, link an Open action (followed
through Theia's opener service, http(s) only).

notifyEditor existed only as a design

The bridge contract has described it since it was written
(docs/theia-bridge-contract-v1.md §4, ADR-0010). This implements it on both
sides:

  • Theia — the protocol shapes, StudioRuntimeClient.onNotifyEditor, a
    frontend controller that shows the message through Theia's MessageService,
    the node endpoint that broadcasts to the browser clients, and the
    /internal/theia/v1/notifyEditor control route. Same shape as openInEditor
    and for the same reason: the node backend has no UI of its own, so the message
    goes to the clients that have one.
  • studio-backendTheiaControlClientV1::notify_editor plus the
    NotifyEditor models, and a second destination in studio-notify.

Two honest limits, both visible in the API

A toast is only worth sending to somebody who is there. The accept path
resolves the workspace to a live session and refuses when there is none, with
a sentence naming the workspace and suggesting a chat connection instead. A chat
message waits in a channel; an IDE notification has nobody to wait for.

Reaching a session is not the same as being seen. A session can run with no
browser tab attached. The bridge answers shown: false for that, and the run
succeeds with a summary saying exactly that — neither a failure to retry nor a
delivery to celebrate. A caller that needs certainty a person saw something
should not be using a toast for it.

Everything the bridge itself fails with is retried (a session restarting, a
control port not yet listening); there is no permanent case, so the attempt cap
ends it and the dead letter carries the last reason.

The feature gate

The IDE bridge is behind the opt-in theia-bridge feature, so the two places
that talk to it are gated — but the wire shape is not. A build without the
bridge still takes workspace_id and answers with the reason it cannot serve
it, rather than having a route whose schema changes with a build flag.

Verification

Backend: clippy -D warnings clean and the suite green in both feature
builds — 275 tests default, 277 with theia-bridge.

On a running stack (config/postgres.yaml,
--no-default-features --features theia-bridge), all four refusals are real:
naming neither destination and naming both are 400s with the same sentence;
level: "critical" is refused with expected info | warn | error; and a
workspace with no session answers "no IDE session to notify for workspace … .
Start a session, or send this to a chat connection instead"
— where the inner
reason names theia_control_enabled when the bridge is off, and the missing
session when studio-session is live.

Theia extension: tsc --noEmit clean for every file this touches, and
studio-backend-module.test.ts passes with two new cases — shown: true for a
client that can display a notification, shown: false for a session whose
clients cannot.

Not verified here, and worth knowing. The toast itself needs a Theia image
built from this source with a browser in front of it; that image build pulls the
whole Theia plugin set and wants a registry secret, so CI's theia job is the
first place this code runs for real. And in the whole-extension jest run four
suites fail in my local harness (the published image's node_modules against
the repo's current jest config) — identically with and without these changes,
and none of them a file this PR touches.

… chat

Studio knows things the person working does not — an import finished, a publish
was rejected, a schedule fired — and until now the only way to tell them was a
chat message they may not be reading. `POST /studio-notify/v1/messages` takes a
`workspace_id` instead of a `connection_id` and the message is shown in the
Theia IDE of whoever has that workspace open.

Same queue, same run, same retries: the editor is a second kind of
*destination*, not a second notification system. `title` becomes the headline
the IDE leads with, `text` the line beneath it, `link` an *Open* action
(followed through Theia's opener service, http(s) only), and `level` is
`info` | `warn` | `error` — an invented one is refused rather than shown as grey
information.

**`notifyEditor` existed only as a design.** The bridge contract (ADR-0010 §4)
has described it since the bridge was written; this implements it on both sides:
the protocol shapes and the `StudioRuntimeClient.onNotifyEditor` callback, a
frontend controller that shows it through Theia's `MessageService`, the node
endpoint that broadcasts to the browser clients, the `/notifyEditor` control
route, and `TheiaControlClientV1::notify_editor` on the Rust side. It is the
same shape as `openInEditor` and for the same reason: the node backend has no UI
of its own, so the message goes to the clients that have one.

**Two honest limits, both visible in the API.** A toast is only worth sending to
somebody who is there, so the accept path resolves the workspace to a *live*
session and refuses when there is none — with a sentence naming the workspace
and suggesting a chat connection instead. And reaching a session is not the same
as being seen: a session with no browser tab attached answers `shown: false`,
and the run succeeds with a summary saying exactly that, because it is neither a
failure to retry nor a delivery to celebrate.

The IDE bridge is behind the opt-in `theia-bridge` feature, so the two places
that talk to it are gated — but the wire shape is not: a build without the
bridge still takes `workspace_id` and answers with the reason it cannot serve
it, rather than having a route whose schema changes with a build flag.

Verified. Backend: clippy `-D warnings` clean and the suite green in both
feature builds (275 tests default, 277 with `theia-bridge`). On a running stack
(config/postgres.yaml, `--no-default-features --features theia-bridge`) all four
refusals are real: naming neither destination and naming both are 400s with the
same sentence; `level: "critical"` is refused; and a workspace with no session
answers "no IDE session to notify for workspace … Start a session, or send this
to a chat connection instead" — with the bridge off, the inner reason names
`theia_control_enabled`, and with studio-session live it names the missing
session. Theia extension: `tsc --noEmit` clean for every file this touches, and
`studio-backend-module.test.ts` passes with two new cases covering `shown: true`
for a client that can display a notification and `shown: false` for a session
whose clients cannot.

Two things I could not verify here, said plainly. The toast itself needs a Theia
image built from this source and a browser in front of it; the image build pulls
the whole Theia plugin set and wants a registry secret, so CI's theia job is the
first place that runs this code for real. And in the whole-extension jest run,
four suites fail in my harness (the published image's `node_modules` against the
repo's current jest config) — identically with and without these changes, and
none of them a file this commit touches.

Signed-off-by: Andrej Kuchma <Andrej.Kuchma@constructor.tech>
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5c073c4e-b80a-41d4-9bf8-1b061cfab08b


Comment @coderabbitai help to get the list of available commands.

@AndrejK666
AndrejK666 merged commit 11d0dc0 into constructorfabric:main Sep 9, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant