Editors: answer without the app running, and sign in to a Revue account from the command line #66

Closed
opened 2026-11-11 23:46:03 +01:00 by thatrichan · 4 comments
Owner

Editor plugins (the JetBrains one in jetbrains-plugins) ask Revue through the revue command: --pulls, --branch-pull, --pending, --comment, --pull, --worktree, --fetch-branch. Today every one of them needs the desktop app running: without it they exit with 3, "Revue isn't running", and the plugin can only offer to start it.

Goal: the revue command answers by itself when no app is running, with this computer's sign-ins or a Revue account signed in from the command line. Revue must still be installed, but not running.

Why this way

The alternatives were weighed and rejected:

  • The plugin calling GitHub, Bitbucket and Forgejo itself would mean decrypting the vault in Kotlin, holding every token in the IDE (Slack and Jira too), rewriting the listing logic, and drafts that can't be shared with the app.
  • revue-server answering the questions would mean the server holding readable tokens, which breaks the account's end-to-end promise. It also can't do worktrees or fetching branches.
  • A background revue serve answers no editor questions today. It would need the same core work as this, plus a port and a process to manage.

This way, no tokens enter the IDE, and drafts stay in the same files the app uses. Every editor feature keeps working: lists, branch status, pending comments, details, worktrees, fetching branches, and markers in files.

Phase 1: answers without the window

  • New module src/ide_answer.rs.
    • The bodies of App::answer's handlers (answer_pulls, answer_branch_pull, answer_pending, answer_comment, answer_pull, answer_worktree, answer_fetch_branch) move behind a trait IdeHost.
    • The trait's methods: repos_for(remotes), account_for(&PrKey) -> Option<Provider>, viewer_login, link_rules, loaded_pr, drafts(key), add_draft(key, d).
    • App implements it with its tabs and globals.
    • A headless implementation uses store::load_*, building providers like serve::proxy::Accounts::load.
  • print_answer (src/lib.rs): with --headless, when url_handler::ask gives NotRunning, answer in-process instead of exiting with 3.
    • Before answering: restore the account (account::Remote::restore(.., auto=false)), then sync(), so renewals made by other computers are picked up.
    • After answering: sync() again, so tokens renewed here are pushed before exiting.
    • An advisory lock in the data folder is held around the whole answer, so two headless processes never spend the same single-use refresh token.
    • answer_comment's write to disk takes the same lock, so an app starting up can't lose a draft written at that moment.
  • ide::EDITOR_INTERFACE goes to 2, which revue --version prints as editor 2.
  • A new exit code 4, "not signed in for this repository", with {"error":…,"sign_in":true}.
  • Docs: docs/command-line.md, "What editors can ask".

Phase 2: account commands

  • revue --account prints {"signed_in":bool,"email":…,"server":…,"sign_ins":[{"service":"github","account":"alice","where":"account|computer"}]}. where matches the docs' "In your Revue account" and "This computer only".
  • revue --account-sign-in [--server URL] [--device NAME] uses the browser sign-in the app uses (account::browser::start/finish, loopback on 47392).
    • It first prints {"url":…}, so an editor can offer "Open the browser again", then prints the --account JSON. Exit 1 when cancelled or expired; the editor cancels by ending the process.
    • The device is stored as the app stores it, so the desktop app shares it.
    • It refuses while a running app answers, saying to sign in there.
  • No server changes.

Phase 3: the JetBrains plugin

  • A new setting whenNotRunning: answer anyway (the default with editor 2 or later), offer to start Revue, or start it. The plugin adds --headless to match.
  • Settings | Tools | Revue gets a Revue account section: its status from --account, and Sign in to Revue…, which runs --account-sign-in with Cancel and opens the URL in the browser.
  • Signing out stays in the desktop app, since it signs the app out too.
  • The tool window and branch status:
    • On exit 4: "Sign in to Revue…".
    • "Start Revue" stays as a second choice.
    • A small note says the answers come without the app.

Tests

  • Rust:
    • A fake IdeHost: each answer's JSON is the same as today's.
    • Headless against the mock services and the fake account: a renewal is pushed before exiting, two processes contend for the lock, and the exit codes are right.
    • --account-sign-in against the fake server, like src/account/browser_tests.rs.
  • Plugin: the --headless arguments, reading exit 4 and the --account JSON, and the settings round trip.

Decided

  • Revue must be installed; it needn't be running.
  • Answering without the app is the default, and a setting can turn it off.
  • The plugin doesn't sign out of the Revue account.

Later, maybe

  • For remote development (Gateway), signing in by pasting the URL, as the app already allows.
  • A background daemon, if one process per question (an estimated 100–300 ms each for the sync) proves too slow.
Editor plugins (the JetBrains one in [jetbrains-plugins](https://forgejo.cleangame.dev/thatrichan/jetbrains-plugins/src/branch/main/revue)) ask Revue through the `revue` command: `--pulls`, `--branch-pull`, `--pending`, `--comment`, `--pull`, `--worktree`, `--fetch-branch`. Today every one of them needs the desktop app running: without it they exit with 3, "Revue isn't running", and the plugin can only offer to start it. **Goal:** the `revue` command answers by itself when no app is running, with this computer's sign-ins or a Revue account signed in from the command line. Revue must still be installed, but not running. ## Why this way The alternatives were weighed and rejected: - The plugin calling GitHub, Bitbucket and Forgejo itself would mean decrypting the vault in Kotlin, holding every token in the IDE (Slack and Jira too), rewriting the listing logic, and drafts that can't be shared with the app. - revue-server answering the questions would mean the server holding readable tokens, which breaks the account's end-to-end promise. It also can't do worktrees or fetching branches. - A background `revue serve` answers no editor questions today. It would need the same core work as this, plus a port and a process to manage. This way, no tokens enter the IDE, and drafts stay in the same files the app uses. Every editor feature keeps working: lists, branch status, pending comments, details, worktrees, fetching branches, and markers in files. ## Phase 1: answers without the window - New module `src/ide_answer.rs`. - The bodies of `App::answer`'s handlers (`answer_pulls`, `answer_branch_pull`, `answer_pending`, `answer_comment`, `answer_pull`, `answer_worktree`, `answer_fetch_branch`) move behind a `trait IdeHost`. - The trait's methods: `repos_for(remotes)`, `account_for(&PrKey) -> Option<Provider>`, `viewer_login`, `link_rules`, `loaded_pr`, `drafts(key)`, `add_draft(key, d)`. - `App` implements it with its tabs and globals. - A headless implementation uses `store::load_*`, building providers like `serve::proxy::Accounts::load`. - `print_answer` (`src/lib.rs`): with `--headless`, when `url_handler::ask` gives `NotRunning`, answer in-process instead of exiting with 3. - Before answering: restore the account (`account::Remote::restore(.., auto=false)`), then `sync()`, so renewals made by other computers are picked up. - After answering: `sync()` again, so tokens renewed here are pushed before exiting. - An advisory lock in the data folder is held around the whole answer, so two headless processes never spend the same single-use refresh token. - `answer_comment`'s write to disk takes the same lock, so an app starting up can't lose a draft written at that moment. - `ide::EDITOR_INTERFACE` goes to 2, which `revue --version` prints as `editor 2`. - A new exit code 4, "not signed in for this repository", with `{"error":…,"sign_in":true}`. - Docs: `docs/command-line.md`, "What editors can ask". ## Phase 2: account commands - `revue --account` prints `{"signed_in":bool,"email":…,"server":…,"sign_ins":[{"service":"github","account":"alice","where":"account|computer"}]}`. `where` matches the docs' "In your Revue account" and "This computer only". - `revue --account-sign-in [--server URL] [--device NAME]` uses the browser sign-in the app uses (`account::browser::start`/`finish`, loopback on 47392). - It first prints `{"url":…}`, so an editor can offer "Open the browser again", then prints the `--account` JSON. Exit 1 when cancelled or expired; the editor cancels by ending the process. - The device is stored as the app stores it, so the desktop app shares it. - It refuses while a running app answers, saying to sign in there. - No server changes. ## Phase 3: the JetBrains plugin - A new setting `whenNotRunning`: answer anyway (the default with `editor 2` or later), offer to start Revue, or start it. The plugin adds `--headless` to match. - Settings | Tools | Revue gets a **Revue account** section: its status from `--account`, and **Sign in to Revue…**, which runs `--account-sign-in` with Cancel and opens the URL in the browser. - Signing out stays in the desktop app, since it signs the app out too. - The tool window and branch status: - On exit 4: "Sign in to Revue…". - "Start Revue" stays as a second choice. - A small note says the answers come without the app. ## Tests - **Rust:** - A fake `IdeHost`: each answer's JSON is the same as today's. - Headless against the mock services and the fake account: a renewal is pushed before exiting, two processes contend for the lock, and the exit codes are right. - `--account-sign-in` against the fake server, like `src/account/browser_tests.rs`. - **Plugin:** the `--headless` arguments, reading exit 4 and the `--account` JSON, and the settings round trip. ## Decided - Revue must be installed; it needn't be running. - Answering without the app is the default, and a setting can turn it off. - The plugin doesn't sign out of the Revue account. ## Later, maybe - For remote development (Gateway), signing in by pasting the URL, as the app already allows. - A background daemon, if one process per question (an estimated 100–300 ms each for the sync) proves too slow.
Author
Owner

Also covered: opening a Slack thread from an editor, revue --open-slack <slack://…> --web <permalink>.

It already works without the app: the revue process itself opens the thread as Open in Slack does, in Slack's app or the browser per Settings → Apps → Slack, falling back to the browser when the app doesn't start. For the browser it hands the address back to the editor ({"opened":"browser","url":…}) instead of opening one itself.

It must keep working in the command-only mode this issue adds, and in any later headless setup:

  • It reads only this computer's settings (store::load_settings) and finds the Slack app (apps::find_slack).
  • It needs no running app and no sign-in, since the thread's slack:// link comes from --pull.
  • When the headless answers move into ide_answer.rs, --open-slack stays a local command, not a question to an IdeHost.
  • A test for it belongs with phase 1's headless tests: --open-slack with no app running answers opened and exits 0.
Also covered: **opening a Slack thread from an editor**, `revue --open-slack <slack://…> --web <permalink>`. It already works without the app: the `revue` process itself opens the thread as **Open in Slack** does, in Slack's app or the browser per **Settings → Apps → Slack**, falling back to the browser when the app doesn't start. For the browser it hands the address back to the editor (`{"opened":"browser","url":…}`) instead of opening one itself. It must keep working in the command-only mode this issue adds, and in any later headless setup: - It reads only this computer's settings (`store::load_settings`) and finds the Slack app (`apps::find_slack`). - It needs no running app and no sign-in, since the thread's `slack://` link comes from `--pull`. - When the headless answers move into `ide_answer.rs`, `--open-slack` stays a local command, not a question to an `IdeHost`. - A test for it belongs with phase 1's headless tests: `--open-slack` with no app running answers `opened` and exits 0.
Author
Owner

Phase 1 is done in 11bd65a (the refactor), 83e2fbb (the feature) and 8c29f3e (docs).

  • The host: src/ide_answer.rs has trait IdeHost. The app implements it with AppHost; the Headless host loads accounts, Jira, Slack and link rules from disk.
  • Answering: with --headless, when no app answers, revue answers in-process. It restores the account and syncs before answering, and syncs again after.
  • The lock: editor.lock in the data folder, using std::fs::File::try_lock, held for the whole headless answer. An app that starts during an answer waits for it (up to 90 s) before writing instance-port. The app's own disk draft write takes the lock too.
  • Exit code 4: {"error":…,"sign_in":true}. The running app's answers carry sign_in too.
  • Versions: EDITOR_INTERFACE is 2.
  • --open-slack: stays local, with a test.

Choices that differ from the plan:

  • --headless is opt-in, so the plugin has to pass it.
  • With no app, there are no loaded tabs, so every answer goes to the services.
  • Headless, the account used is the first one for the host.

Left: phase 2 (--account, --account-sign-in), and phase 3 in the plugin's repository. Not yet tried against live services.

Phase 1 is done in 11bd65a (the refactor), 83e2fbb (the feature) and 8c29f3e (docs). - **The host:** `src/ide_answer.rs` has `trait IdeHost`. The app implements it with `AppHost`; the `Headless` host loads accounts, Jira, Slack and link rules from disk. - **Answering:** with `--headless`, when no app answers, `revue` answers in-process. It restores the account and syncs before answering, and syncs again after. - **The lock:** `editor.lock` in the data folder, using `std::fs::File::try_lock`, held for the whole headless answer. An app that starts during an answer waits for it (up to 90 s) before writing `instance-port`. The app's own disk draft write takes the lock too. - **Exit code 4:** `{"error":…,"sign_in":true}`. The running app's answers carry `sign_in` too. - **Versions:** `EDITOR_INTERFACE` is 2. - **`--open-slack`:** stays local, with a test. Choices that differ from the plan: - `--headless` is opt-in, so the plugin has to pass it. - With no app, there are no loaded tabs, so every answer goes to the services. - Headless, the account used is the first one for the host. Left: phase 2 (`--account`, `--account-sign-in`), and phase 3 in the plugin's repository. Not yet tried against live services.
Author
Owner

Phase 2 is done in e16d7ec and 80c10c6.

revue --account prints {"signed_in":…,"email":…,"server":…,"device":…,"sign_ins":[{"service","account","where":"account|computer"}]}. The running app answers it, or else the command itself does.

revue --account-sign-in [--server URL] [--device NAME]:

  • It prints {"url":…} and opens the browser, the same browser sign-in the app uses. After approval it prints the --account JSON. Otherwise it prints {"error":…} and exits 1: cancelled, the 10-minute wait ran out, or a bad server.
  • While an app is running, it refuses with "app_running":true.
  • The browser wait holds no lock. Saving the sign-in is under DataLock, and the command checks again that no app has started.

The commands are editor 6; editor 5 is the resolve and submit options from db1b19d. Both are in docs/command-line.md.

Phase 3: the plugin already passes --headless and handles exit 4 (revue-companion 2806a19). The plugin's Revue account section in its settings is in progress.

Phase 2 is done in e16d7ec and 80c10c6. **`revue --account`** prints `{"signed_in":…,"email":…,"server":…,"device":…,"sign_ins":[{"service","account","where":"account|computer"}]}`. The running app answers it, or else the command itself does. **`revue --account-sign-in [--server URL] [--device NAME]`:** - It prints `{"url":…}` and opens the browser, the same browser sign-in the app uses. After approval it prints the `--account` JSON. Otherwise it prints `{"error":…}` and exits 1: cancelled, the 10-minute wait ran out, or a bad server. - While an app is running, it refuses with `"app_running":true`. - The browser wait holds no lock. Saving the sign-in is under `DataLock`, and the command checks again that no app has started. The commands are `editor 6`; `editor 5` is the resolve and submit options from db1b19d. Both are in `docs/command-line.md`. Phase 3: the plugin already passes `--headless` and handles exit 4 (revue-companion 2806a19). The plugin's **Revue account** section in its settings is in progress.
Author
Owner

Phase 3 is done in revue-companion 0142ed1 (headless answers and exit 4 were already in 2806a19).

  • Settings | Tools | Revue Companion has new parts:
    • When the Revue app isn't running: answer without it (the default), offer to start it, or start it.
    • Revue Account: shown with editor >= 6. It shows the status from --account: the email, server, device, and each sign-in marked "In your Revue account" or "This computer only".
    • Sign in to Revue… runs --account-sign-in. While it waits, it offers Open the browser again and Cancel.
    • When an app is running, it shows Revue's message and Bring Revue forward.
    • There's no sign-out: the page says to sign out in the app.
  • Exit 4: the tool window offers Sign in to Revue… first, then Open Revue and Retry. The branch status offers the same once, as a notification. A note says the answers come without the app.
  • Tests: 62 unit tests pass offline, 7 of them new in AccountTest.

Not yet tried in a running IDE (runIde, verifyPlugin) against a real editor 6 Revue.

Every phase is done, so I'm closing this.

Phase 3 is done in revue-companion 0142ed1 (headless answers and exit 4 were already in 2806a19). - **Settings | Tools | Revue Companion** has new parts: - **When the Revue app isn't running:** answer without it (the default), offer to start it, or start it. - **Revue Account:** shown with `editor >= 6`. It shows the status from `--account`: the email, server, device, and each sign-in marked "In your Revue account" or "This computer only". - **Sign in to Revue…** runs `--account-sign-in`. While it waits, it offers **Open the browser again** and **Cancel**. - When an app is running, it shows Revue's message and **Bring Revue forward**. - There's no sign-out: the page says to sign out in the app. - **Exit 4:** the tool window offers **Sign in to Revue…** first, then Open Revue and Retry. The branch status offers the same once, as a notification. A note says the answers come without the app. - **Tests:** 62 unit tests pass offline, 7 of them new in `AccountTest`. Not yet tried in a running IDE (`runIde`, `verifyPlugin`) against a real `editor 6` Revue. Every phase is done, so I'm closing this.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
thatrichan/revue#66
No description provided.