@oar-sh/oar 0.9.25 → 0.9.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +9 -9
  3. package/package.json +3 -1
  4. package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
  5. package/server/claude-worker/claude-session-process.mjs +4 -0
  6. package/server/claude-worker/claude-turn-publisher.mjs +25 -11
  7. package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
  8. package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
  9. package/server/grok-worker/grok-turn-runner.mjs +3 -1
  10. package/server/public/app/context-usage-view.mjs +2 -2
  11. package/server/public/app/live-activity-cap.mjs +3 -3
  12. package/server/public/app/markdown-math.mjs +471 -0
  13. package/server/public/app/pwa-install.js +25 -4
  14. package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
  15. package/server/public/app/relay-restart-ui.js +2 -2
  16. package/server/public/app/remote-relays-settings.js +33 -6
  17. package/server/public/app/router.js +49 -237
  18. package/server/public/app/settings-tabs.js +10 -1
  19. package/server/public/app/store.js +2 -0
  20. package/server/public/app/stream-view-preference.mjs +1 -1
  21. package/server/public/app/thoughts-preference.mjs +1 -1
  22. package/server/public/app/tmux-inspector-view.js +29 -16
  23. package/server/public/index.html +134 -116
  24. package/server/routes/relay-board-routes.mjs +46 -1
  25. package/server/routes/remote-relay-routes.mjs +1 -1
  26. package/server/server-runtime.mjs +2 -0
  27. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  28. package/server/services/relay-restart-queue.mjs +1 -1
  29. package/server/services/remote-relay-dispatcher.mjs +23 -1
  30. package/server/services/remote-relay-registry-service.mjs +7 -1
  31. package/server/services/session-worker-stop-service.mjs +50 -18
  32. package/shared/math-instructions.mjs +30 -0
  33. package/server/public/app/math-dollars.mjs +0 -89
package/CHANGELOG.md CHANGED
@@ -3,6 +3,148 @@
3
3
  All notable changes to OAR are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow semver.
5
5
 
6
+ ## [0.9.26] — 2026-11-11
7
+
8
+ Highlights: formulas from their delimiters only, and Settings sorted by
9
+ where they live. Replies render a formula only when it is written as
10
+ `$…$`, `\(…\)`, `$$…$$`, `\[…\]` or a fenced `math` block, so an id in
11
+ parentheses, a Windows path, a price or a shell variable stays text, a
12
+ formula KaTeX cannot read is shown as written instead of in red, and
13
+ the agent is told the notation that renders, except in Claude Cloud
14
+ chats and on the Copilot Extension engine. KaTeX moves to 0.19.1.
15
+ Theme, text scaling and the live-turn settings get an Appearance tab of
16
+ their own, and the relay name and the two restart settings move to
17
+ Relays, where an empty name shows the host name the relay goes by and a
18
+ rename made on another device shows at once. In Claude, Claude API and
19
+ DeepSeek chats a plan card comes only from a plan the agent hands over,
20
+ and a plan card shows up to 200,000 characters of the plan instead of
21
+ 12,000. Archive and delete no longer fail on Linux and macOS for a
22
+ worker that has already ended, a session an agent on a paired relay
23
+ started can always report back to it, and the tmux console draws a real
24
+ terminal.
25
+
26
+ ### Added
27
+
28
+ - **Fenced `math` blocks.** A code block with the language `math` is a
29
+ displayed formula once it is closed.
30
+ - **Agents are told how to write formulas.** Once per session, the agent
31
+ of a Claude, Claude API, DeepSeek, Cursor or Grok chat, and of a
32
+ Copilot or OpenAI chat on the SDK engine, learns that `$…$` in a
33
+ sentence and `$$…$$` on lines of their own render (`\(…\)` and `\[…\]`
34
+ too), that TeX in bare parentheses or brackets does not, and that a
35
+ dollar amount stays text. Claude Cloud chats and the Copilot Extension
36
+ engine do not get the note. The prompt a transcript shows leaves the
37
+ note out.
38
+
39
+ ### Changed
40
+
41
+ - **Settings: an Appearance tab, and the relay's own settings on
42
+ Relays.** Theme, Text scaling, Expand thoughts while an agent works,
43
+ Tool lines shown while an agent works, Stream view and Show Suspend
44
+ host action move from General to the new Appearance tab right after
45
+ it, under one line that says they are kept in this browser; Jump to
46
+ the end when I send a message and Messages kept per conversation stay
47
+ in General's Reading block. Relay name moves to the top of Relays,
48
+ above Public address, and After a relay restart, tell interrupted
49
+ sessions to carry on and Ask after a queued relay restart has waited
50
+ follow right below Public address; what stays of General's "This
51
+ relay" block (Default CWD, Autostart, the Windows helper) is headed
52
+ "This computer". Every setting keeps its value and default, and a
53
+ remembered tab or a link to General still opens General.
54
+ - **Formulas are read from their delimiters only.** `$…$` and `\(…\)`
55
+ inside a sentence, `$$…$$` and `\[…\]` as a display, and a fenced
56
+ `math` block; nothing is guessed from TeX in bare parentheses or
57
+ brackets any more. On one line `\[ … \]` needs a space inside both
58
+ ends, so `\[1\]` and `\[x\]` are escaped brackets. A `$` opens a
59
+ formula only where maths is written that way, so prices (`$5 and $9`,
60
+ `$5/$10`), shell variables (`$HOME/$PATH`) and Windows paths stay
61
+ text, and `\$` is always a dollar sign. Code spans and code blocks
62
+ never hold a formula. A formula KaTeX cannot parse is shown as
63
+ written, in the normal text colour, and a percent sign in a one-line
64
+ formula is shown. A message over 100,000 characters renders no
65
+ formulas. The rules are in
66
+ [Formulas in replies](docs/web-ui.md#formulas-in-replies).
67
+ - **KaTeX 0.19.1.** The page loads KaTeX 0.19.1 (was 0.16.22) and its
68
+ chemistry extension from jsDelivr, as before, now with integrity hashes;
69
+ its auto-render script is no longer loaded. KaTeX is not in the npm
70
+ package: a browser that cannot reach jsDelivr shows every formula as
71
+ written.
72
+ - **A plan card comes from a plan hand-over only** in Claude, Claude API
73
+ and DeepSeek chats. A plan-mode reply with two or more list lines no
74
+ longer gets a "Plan ready for review" card, so a session that stays in
75
+ plan mode no longer puts the buttons under every status reply. When the
76
+ plan of a hand-over cannot be read, the reply stands in for it. Cursor,
77
+ Grok, Copilot and OpenAI (BYOK) chats keep the card for such replies.
78
+
79
+ ### Fixed
80
+
81
+ - **The whole plan on the plan card.** The card cut every plan at 12,000
82
+ characters, without a mark. It now keeps up to 200,000; a longer plan
83
+ is cut, at a line break where it can, and ends with a line that says
84
+ how much is missing and, where the agent named it, the file that holds
85
+ the whole plan. A cut inside a code block closes the block first, also
86
+ in a plan file with Windows line ends.
87
+ - **Archive and delete no longer fail for a worker that has ended**
88
+ (Linux and macOS). Archiving or deleting an idle conversation could
89
+ answer "Couldn't stop this conversation's session" again and again:
90
+ the relay counted the old process id of a worker that had ended, by
91
+ then another program's, and processes that were ending by themselves.
92
+ Those no longer count, and a process the relay may not signal no longer
93
+ keeps it from stopping the others. A worker that really survives the
94
+ stop still leaves the conversation as it was.
95
+ - **A session an agent on a paired relay started can always report
96
+ back.** Its send to the session that started it was refused with
97
+ `REMOTE_RELAY_HOP_LIMIT` when its turn also acted on a prompt from a
98
+ session on its own relay. Another session on that relay, a third relay
99
+ and `create_session` are still held to the limit.
100
+ - **The relay name in Settings.** An empty Relay name showed "OAR" as its
101
+ placeholder; it now shows the host name, the name paired relays know
102
+ the relay by then. A rename made on another device shows in an open
103
+ Settings without reopening it.
104
+ - **The tmux console draws a terminal.** 🖥️ Inspect tmux console asked
105
+ jsDelivr and unpkg for xterm.js versions that were never published, so
106
+ it always fell back to plain text. It now loads `@xterm/xterm` 5.5.0
107
+ and `@xterm/addon-fit` 0.10.0 from jsDelivr, with integrity hashes,
108
+ and only the two scripts from unpkg when jsDelivr fails; a browser
109
+ that reaches neither still gets the plain text.
110
+ - **A long line in a code block no longer runs under its Copy button**,
111
+ which has a strip of its own above the first line.
112
+
113
+ ### Upgrade notes
114
+
115
+ - **No migration.** As before, the relay copies its database to the
116
+ backups folder before the first start of a new version; nothing else
117
+ waits.
118
+ - **Sessions still on 0.9.25's worker wait one worker start.** A session
119
+ whose worker still runs the old code is given a new worker between
120
+ turns, as in 0.9.25: a Claude, Claude API, DeepSeek or Cursor session
121
+ outside a git checkout before its next message, any other once its
122
+ agent has been idle for about 10 minutes. Its agent learns the formula
123
+ notation with the new worker, and Claude, Claude API and DeepSeek
124
+ chats get the new plan-card rule with it.
125
+ - **Your settings stay as they were.** The Appearance settings are kept
126
+ in the browser under the same keys, and the relay's own settings in its
127
+ database; only their place in Settings changes.
128
+ - **Older replies follow the new formula rules** when they are shown
129
+ again: a parenthesis 0.9.25 drew as a formula is text now, TeX
130
+ written in bare parentheses shows as written, and a one-line
131
+ `\[x^2\]` without a space inside its ends is read as escaped brackets.
132
+ - **KaTeX and xterm.js load in the browser,** not from the relay: from
133
+ jsDelivr, and the two xterm.js scripts, not its stylesheet, also from
134
+ unpkg; neither is in the npm package. A browser without access to them
135
+ shows formulas as written and the tmux console as plain text, whatever
136
+ the relay host can reach.
137
+ - **Mixed versions work together.** The report-back rule is checked by
138
+ the relay the started session runs on: that one needs 0.9.26, the relay
139
+ of the session that started it may run 0.9.25.
140
+ - **Going back to 0.9.25** needs no database step. Install it with
141
+ `oar update --to 0.9.25` or `npm install -g @oar-sh/oar@0.9.25` (in a
142
+ git checkout: `git checkout v0.9.25`, then a relay restart). The
143
+ settings go back to their old places with their values; a browser that
144
+ last had Appearance open opens General. Formulas follow 0.9.25's rules
145
+ again, Claude plan-mode replies with list lines get cards again, and
146
+ plan cards made on 0.9.26 keep their whole plan.
147
+
6
148
  ## [0.9.25] — 2026-11-09
7
149
 
8
150
  Highlights: a media gallery, and agents that make images. Every picture,
package/README.md CHANGED
@@ -45,29 +45,29 @@ flowchart LR
45
45
  <table>
46
46
  <tr>
47
47
  <td width="42%" rowspan="4" valign="top">
48
- <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.25" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
48
+ <a href="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-mobile.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/cfmail-mobile.png?v=0.9.26" alt="OAR on a phone: a finished agent turn in a conversation, with the composer below"></picture></a>
49
49
  <br><sub>On a phone: the same conversations, installable as an app.</sub>
50
50
  </td>
51
51
  <td width="58%" valign="top">
52
- <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.25" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
52
+ <a href="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-agents.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/cfmail-agents.png?v=0.9.26" alt="Background subagents running in parallel in the task panel, each with live token counts, elapsed time, and its own Stop button"></picture></a>
53
53
  <br><sub>Agents working in parallel, each with live token counts and its own Stop.</sub>
54
54
  </td>
55
55
  </tr>
56
56
  <tr>
57
57
  <td valign="top">
58
- <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.25" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
58
+ <a href="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-question.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/cfmail-question.png?v=0.9.26" alt="A question card in the middle of a turn: the agent offers several choices and a free-text reply"></picture></a>
59
59
  <br><sub>When the agent needs a decision, it asks, and waits for your answer.</sub>
60
60
  </td>
61
61
  </tr>
62
62
  <tr>
63
63
  <td valign="top">
64
- <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.25" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
64
+ <a href="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pingcf-thoughts.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/pingcf-thoughts.png?v=0.9.26" alt="An expanded Thoughts block showing the agent's reasoning above its reply"></picture></a>
65
65
  <br><sub>Read the reasoning, not just the answer.</sub>
66
66
  </td>
67
67
  </tr>
68
68
  <tr>
69
69
  <td valign="top">
70
- <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.25" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
70
+ <a href="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-watching.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/pagewright-watching.png?v=0.9.26" alt="A session watching two other sessions: a finished reply arrives as a compact relay-event row, the watching 2 sessions list is open with Unsubscribe, and a request to also watch a session on a paired relay waits on an Allow / Deny card"></picture></a>
71
71
  <br><sub>Agents that watch agents: woken by events, approving nothing.</sub>
72
72
  </td>
73
73
  </tr>
@@ -118,11 +118,11 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
118
118
  <table>
119
119
  <tr>
120
120
  <td width="62%" valign="top">
121
- <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.25" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
121
+ <a href="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/cfmail-steering.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/cfmail-steering.png?v=0.9.26" alt="A running Claude turn with a message steered into it mid-turn, the reply streaming with its tool calls, and the Steer button in the composer"></picture></a>
122
122
  <br><sub>Send a message while the agent works, and it steers the running turn.</sub>
123
123
  </td>
124
124
  <td width="38%" valign="top">
125
- <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.25" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
125
+ <a href="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/usage-copilot-compact.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/usage-copilot-compact.png?v=0.9.26" alt="The GitHub Copilot card in Check Usage: premium requests and plan quota with reset dates, and the cost of the last turn"></picture></a>
126
126
  <br><sub>What each plan has left, in one place.</sub>
127
127
  </td>
128
128
  </tr>
@@ -140,7 +140,7 @@ API keys are entered in **⚙️ Settings** and stay on the relay host. The Clau
140
140
  - **Share** a conversation as a read-only link, hide single messages and the host's paths from it, and see when it was opened ([Sharing](docs/web-ui.md#sharing)).
141
141
  - **On your phone**: install OAR as an app and get push notifications; back walks through the conversations you opened, a pinch zooms the whole app, and held sideways it keeps a phone's layout ([Notifications and the app](docs/web-ui.md#notifications-and-the-app), [back and forward](docs/web-ui.md#back-and-forward), [zoom](docs/web-ui.md#zoom), [a phone held sideways](docs/web-ui.md#a-phone-held-sideways)).
142
142
 
143
- The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.25](CHANGELOG.md#0925--2026-11-09) as the latest.
143
+ The full tour is in [Using the web UI](docs/web-ui.md); every setting and config key is in [Settings and configuration](docs/configuration.md). What changed in each release is in the [changelog](CHANGELOG.md), with [0.9.26](CHANGELOG.md#0926--2026-11-11) as the latest.
144
144
 
145
145
  ## Remote access
146
146
 
@@ -191,7 +191,7 @@ flowchart LR
191
191
 
192
192
  You stay in charge: per paired relay you decide whether agents may only read, also prompt, or do everything, and in the `ask` and `plan` modes every write asks you first. A subscription only notifies; it approves nothing.
193
193
 
194
- <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.25"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.25"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.25" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
194
+ <p><a href="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.26"><picture><source media="(prefers-color-scheme: light)" srcset="https://oar.sh/assets/shots/pagewright-cross-relay.light.png?v=0.9.26"><img src="https://oar.sh/assets/shots/pagewright-cross-relay.png?v=0.9.26" alt="A session watching two sessions on a paired relay: a reply arrives as a relay-event row, and after a short outage the events the other relay kept arrive as one replayed row" width="720"></picture></a></p>
195
195
 
196
196
  How pairing, permissions and limits work is in [Agents working together](docs/agents-together.md).
197
197
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oar-sh/oar",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "type": "module",
5
5
  "description": "OAR — Open Agent Relay: drive your local coding agents from any browser",
6
6
  "license": "MIT",
@@ -74,7 +74,9 @@
74
74
  "@modelcontextprotocol/sdk": "1.32.2",
75
75
  "@playwright/test": "^1.60.0",
76
76
  "jsdom": "^30.0.1",
77
+ "katex": "0.19.1",
77
78
  "koffi": "3.3.4",
79
+ "marked": "15.0.12",
78
80
  "zod": "4.6.7"
79
81
  },
80
82
  "overrides": {
@@ -3,6 +3,7 @@ import { loadAgentSdk } from '../agent-sdk-loader.mjs';
3
3
  import { formatClaudeBackgroundCommandLimitLabel } from '../../shared/claude-background-command-limit.mjs';
4
4
  import { parseThinkingDisplay } from '../../shared/claude-thinking.mjs';
5
5
  import { PERSON_ONLY_QUESTION_SOURCE } from '../../shared/relay-asks.mjs';
6
+ import { MATH_INSTRUCTION_TEXT } from '../../shared/math-instructions.mjs';
6
7
  import { MEDIA_EMBED_INSTRUCTION_TEXT } from '../../shared/media-embed-instructions.mjs';
7
8
  import { REMOTE_RELAY_TOOL_CALL_TIMEOUT_MS } from '../../shared/remote-relay-tool-core.mjs';
8
9
  import { createPreviewToolDefinition } from './claude-preview-tool.mjs';
@@ -205,10 +206,12 @@ export function longCommandInstructionText(backgroundCommandLimitMs = null) {
205
206
  export function systemPromptForRelayMode(relayMode, { backgroundCommandLimitMs = null } = {}) {
206
207
  const mode = String(relayMode || 'agent').trim().toLowerCase();
207
208
  // The media-embed capability rides every session — unlike the mode nudges it
208
- // is renderer knowledge the model cannot discover any other way. The long
209
- // command note rides every session too: the limit is the relay's setting.
209
+ // is renderer knowledge the model cannot discover any other way; so is the
210
+ // notation the web app renders formulas from. The long command note rides
211
+ // every session too: the limit is the relay's setting.
210
212
  const append = [
211
213
  MEDIA_EMBED_INSTRUCTION_TEXT,
214
+ MATH_INSTRUCTION_TEXT,
212
215
  longCommandInstructionText(backgroundCommandLimitMs),
213
216
  MODE_SYSTEM_PROMPT_APPEND[mode],
214
217
  ]
@@ -792,8 +795,9 @@ const EXIT_PLAN_DENY_MESSAGE = 'Plan received — it is shown to the user in the
792
795
  + 'End the turn now with a short closing message and do not start implementing; '
793
796
  + 'the user\'s decision will arrive as a new message.';
794
797
  // When the handler could not surface a board (no plan text reached it), the
795
- // model must restate the plan as its final message: plan-shaped final text
796
- // triggers the runner's fallback board, so the user still gets the review UI.
798
+ // model must restate the plan as its final message: after a hand-over without
799
+ // a card, the turn's reply is posted as the plan (publishCompletedTurn), so
800
+ // the user still gets the review UI.
797
801
  const EXIT_PLAN_DENY_NO_BOARD_MESSAGE = 'Plan received, but it could not be shown for review. '
798
802
  + 'Restate the complete plan as your final message, then end the turn without implementing; '
799
803
  + 'the user\'s decision will arrive as a new message.';
@@ -876,6 +876,8 @@ export function createClaudeSessionRunner({
876
876
  modelUsage: null,
877
877
  },
878
878
  planBoardPosted: false,
879
+ // The CLI handed a plan over (ExitPlanMode), whether or not a card came of it.
880
+ planBoardRequested: false,
879
881
  // The CLI process the turn ran in, as its usage report names it: taken
880
882
  // from the process here and again when the context is made active, and
881
883
  // read from the context at finalize — a late continuation flush
@@ -1569,6 +1571,7 @@ export function createClaudeSessionRunner({
1569
1571
  state: ctx.state,
1570
1572
  responseModel,
1571
1573
  planBoardPosted: ctx.planBoardPosted,
1574
+ planBoardRequested: ctx.planBoardRequested,
1572
1575
  consumedSteerIds,
1573
1576
  });
1574
1577
  }
@@ -3789,6 +3792,7 @@ export function createClaudeSessionRunner({
3789
3792
  onExitPlanMode: async (input) => {
3790
3793
  const ctx = processRef.activeCtx || processRef.pendingDelivered[0]?.ctx || null;
3791
3794
  if (!ctx?.message?.id) return false;
3795
+ ctx.planBoardRequested = true;
3792
3796
  const posted = await publisher.publishPlanBoard(ctx.message, input);
3793
3797
  if (posted) ctx.planBoardPosted = true;
3794
3798
  return posted;
@@ -6,7 +6,6 @@ import {
6
6
  classifyClaudeResultFailure,
7
7
  claudeFailureGuidance,
8
8
  } from './claude-turn-failure.mjs';
9
- import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
10
9
  import { PLAN_BOARD_ACTIONS } from '../../shared/plan-board-actions.mjs';
11
10
  import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
12
11
  import { buildSteerSettleFailure, steerSettleFailureText } from '../../shared/steer-settle-failure.mjs';
@@ -34,9 +33,10 @@ export function isStaleAttemptError(error) {
34
33
  export { buildSteerSettleFailure, steerSettleFailureText };
35
34
  export const STEER_SETTLE_FAILED_TEXT = steerSettleFailureText('folded');
36
35
 
37
- export function buildClaudePlanReadyBoardPayload({ message, planText = '' } = {}) {
36
+ export function buildClaudePlanReadyBoardPayload({ message, planText = '', planFilePath = '' } = {}) {
38
37
  const summary = String(planText || '').trim();
39
38
  if (!summary) return null;
39
+ const filePath = String(planFilePath || '').trim();
40
40
  return {
41
41
  queueId: message?.id,
42
42
  messageId: message?.id,
@@ -52,6 +52,8 @@ export function buildClaudePlanReadyBoardPayload({ message, planText = '' } = {}
52
52
  queueMessageId: message?.id || null,
53
53
  conversationId: message?.conversationId || null,
54
54
  relayMode: message?.relayMode || 'agent',
55
+ // Named on the card when the relay has to cut a very long plan.
56
+ ...(filePath ? { planFilePath: filePath } : {}),
55
57
  },
56
58
  };
57
59
  }
@@ -797,13 +799,13 @@ export function createClaudeTurnPublisher({
797
799
  * Post the plan-ready review board for an ExitPlanMode interception. The CLI
798
800
  * sends both `plan` and `planFilePath`; the inline plan is authoritative, the
799
801
  * plan file covers CLI versions that stop inlining it. Returns false when no
800
- * plan text could be recovered so the caller's plan-shaped-final-text
801
- * fallback still gets a chance to post the board.
802
+ * plan text could be recovered, so the turn's reply can stand in for it
803
+ * (see publishCompletedTurn).
802
804
  */
803
805
  async function publishPlanBoard(message, input) {
804
806
  let planText = String(input?.plan || input?.summary || '').trim();
807
+ const planFilePath = String(input?.planFilePath || '').trim();
805
808
  if (!planText) {
806
- const planFilePath = String(input?.planFilePath || '').trim();
807
809
  if (planFilePath) {
808
810
  try {
809
811
  planText = String(fs.readFileSync(planFilePath, 'utf8') || '').trim();
@@ -812,7 +814,7 @@ export function createClaudeTurnPublisher({
812
814
  }
813
815
  }
814
816
  }
815
- const boardPayload = buildClaudePlanReadyBoardPayload({ message, planText });
817
+ const boardPayload = buildClaudePlanReadyBoardPayload({ message, planText, planFilePath });
816
818
  if (!boardPayload) return false;
817
819
  await api('POST', '/api/relay-board', boardPayload).catch((error) => {
818
820
  dbg('plan board publish failed', error?.message || String(error));
@@ -822,18 +824,30 @@ export function createClaudeTurnPublisher({
822
824
 
823
825
  /**
824
826
  * Publish a completed (non-error, non-aborted) turn: final stream snapshot,
825
- * plan-mode fallback board, response row. Mirrors the pre-session-process
826
- * turn runner's tail exactly.
827
+ * plan-mode fallback board, response row.
828
+ *
829
+ * A plan card comes only from a plan hand-over (ExitPlanMode). The fallback
830
+ * covers one case: the CLI handed a plan over but its text could not be
831
+ * read, so the reply is the plan. A reply is never taken for a plan because
832
+ * of its list lines: in a session that stays in plan mode, every status
833
+ * reply with two bullets got a card.
827
834
  */
828
- async function publishCompletedTurn({ message, state, responseModel, planBoardPosted, consumedSteerIds = null }) {
835
+ async function publishCompletedTurn({
836
+ message,
837
+ state,
838
+ responseModel,
839
+ planBoardPosted,
840
+ planBoardRequested = false,
841
+ consumedSteerIds = null,
842
+ }) {
829
843
  const result = state.result;
830
844
  const finalText = String(
831
845
  state.resultTexts.join('\n\n') || result?.text || state.lastStreamedText || '',
832
846
  ).trim();
833
847
  if (
834
- !planBoardPosted
848
+ planBoardRequested
849
+ && !planBoardPosted
835
850
  && String(message.relayMode || '').trim().toLowerCase() === 'plan'
836
- && countPlanLikeLines(finalText) >= 2
837
851
  ) {
838
852
  const boardPayload = buildClaudePlanReadyBoardPayload({ message, planText: finalText });
839
853
  if (boardPayload) {
@@ -25,6 +25,7 @@ import {
25
25
  applyPreviewInstructions,
26
26
  createPreviewInstructionsProvider,
27
27
  } from '../../shared/preview-instructions.mjs';
28
+ import { applyMathInstructions } from '../../shared/math-instructions.mjs';
28
29
  import { applyMediaEmbedInstructions } from '../../shared/media-embed-instructions.mjs';
29
30
  import { buildModePrompt } from '../../shared/worker-runtime/prompt-context.mjs';
30
31
  import { loadRelayInstructionsFromFile } from '../../shared/worker-runtime/config-loader.mjs';
@@ -84,7 +85,7 @@ export function createCopilotPromptContextBuilder({
84
85
  .then(() => getPreviewInstructions({ conversationId: String(message?.conversationId || '') }))
85
86
  .catch(() => '')
86
87
  : '';
87
- instructions = applyMediaEmbedInstructions(applyPreviewInstructions(toolInstructions, previewBlock));
88
+ instructions = applyMathInstructions(applyMediaEmbedInstructions(applyPreviewInstructions(toolInstructions, previewBlock)));
88
89
  }
89
90
  const prefix = buildModePrompt(relayMode, instructions, { includeInstructions });
90
91
  return {
@@ -20,6 +20,7 @@ import {
20
20
  cursorSubagentRosterFingerprint,
21
21
  } from './cursor-subagent-roster.mjs';
22
22
  import { createAskUserBridge } from '../../shared/ask-user-bridge.mjs';
23
+ import { renderMathInstructionBlock } from '../../shared/math-instructions.mjs';
23
24
  import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
24
25
  import { EMPTY_TURN_COMPLETION_NOTE } from '../../shared/empty-turn-completion.mjs';
25
26
  import { resolveFallbackContextLimitTokens } from '../../shared/context-window-fallbacks.mjs';
@@ -586,7 +587,8 @@ export function createCursorTurnRunner({
586
587
  await ensureAgentHandle(message, model);
587
588
  const userMessage = buildUserMessageImpl(message);
588
589
  const modeNudge = cursorModeNudge(message.relayMode, lastNudgedRelayMode);
589
- const mediaGuidance = mediaGuidanceSent ? '' : renderMediaEmbedInstructionBlock();
590
+ // The formula notation rides with it, for the same reason.
591
+ const mediaGuidance = mediaGuidanceSent ? '' : `${renderMediaEmbedInstructionBlock()}\n\n${renderMathInstructionBlock()}`;
590
592
  if (modeNudge || mediaGuidance) {
591
593
  userMessage.text = [mediaGuidance, modeNudge, userMessage.text].filter(Boolean).join('\n\n');
592
594
  }
@@ -10,6 +10,7 @@ import { extractGrokUsageFromPromptResult, normalizeGrokTurnUsage } from '../ser
10
10
  import { OAR_MCP_SERVER_NAME, buildOarMcpServerLaunch, isOarMcpServerDisabled } from '../mcp/oar-mcp-server.mjs';
11
11
  import { createPreviewInstructionsProvider } from '../../shared/preview-instructions.mjs';
12
12
  import { fetchRemoteRelayInflight } from '../../shared/remote-relay-tool-core.mjs';
13
+ import { renderMathInstructionBlock } from '../../shared/math-instructions.mjs';
13
14
  import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
14
15
  import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
15
16
  import { PLAN_BOARD_ACTIONS } from '../../shared/plan-board-actions.mjs';
@@ -476,7 +477,8 @@ export function createGrokTurnRunner({
476
477
  : await Promise.resolve()
477
478
  .then(() => getPreviewInstructions?.({ conversationId: message.conversationId || sdkSessionId || '' }))
478
479
  .catch(() => '');
479
- const mediaBlock = mediaInstructionsSent ? '' : renderMediaEmbedInstructionBlock();
480
+ // The formula notation rides with the media block, once per worker.
481
+ const mediaBlock = mediaInstructionsSent ? '' : `${renderMediaEmbedInstructionBlock()}\n\n${renderMathInstructionBlock()}`;
480
482
  const promptText = [previewBlock, mediaBlock, modeNudge, userText].filter(Boolean).join('\n\n');
481
483
  const pendingNudgedRelayMode = String(message.relayMode || 'agent').trim().toLowerCase();
482
484
 
@@ -356,7 +356,7 @@ const THOUGHTS_EXPANDED_CHOICES = Object.freeze([
356
356
  ]);
357
357
 
358
358
  /**
359
- * The mirror of Settings › General "Expand thoughts while an agent works", for
359
+ * The mirror of Settings › Appearance "Expand thoughts while an agent works", for
360
360
  * every provider. A setting of this browser, so nothing goes to the relay:
361
361
  * the buttons write thoughts-preference.mjs and both places follow it.
362
362
  *
@@ -390,7 +390,7 @@ const STREAM_VIEW_CHOICES = Object.freeze([
390
390
  ]);
391
391
 
392
392
  /**
393
- * The mirror of Settings › General "Stream view", for every provider. A
393
+ * The mirror of Settings › Appearance "Stream view", for every provider. A
394
394
  * setting of this browser (stream-view-preference.mjs); nothing goes to the
395
395
  * relay.
396
396
  *
@@ -1,6 +1,6 @@
1
- // "Tool lines shown while an agent works" (Settings › General ›
2
- // Appearance): how many of its newest tool lines each agent of a running
3
- // turn keeps in the live bubble, the main agent and every sub-agent apart.
1
+ // "Tool lines shown while an agent works" (Settings › Appearance): how many
2
+ // of its newest tool lines each agent of a running turn keeps in the live
3
+ // bubble, the main agent and every sub-agent apart.
4
4
  // A setting of this browser, kept in localStorage like the scroll-on-send
5
5
  // one. The slider runs from 10 to 500 by 10 and has one more step at the far
6
6
  // end, no limit (stored as '0'). The document and the storage are passed in