Commit graph

852 commits

Author SHA1 Message Date
Anish Sarkar
524a5613bb feat(plugins): an OAuth server added by URL is saved signed out and signed in from its row
Adding one no longer starts a sign-in: it answers 201 once the
authorization server's hosts are allowed, and Connect on its row
registers and opens the sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:58:09 +05:30
Anish Sarkar
172aacfcab fix(plugins): a tool's description is no longer shown on hover
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 02:57:42 +05:30
Anish Sarkar
c1a026e945 Merge upstream dev into feat/plugins-homepage
Resolves dev's sign-in failure codes (#2267) and its OCR ADR (#2295)
against this branch:
- dev's ADR 0062 keeps its number; this branch's become 0063, 0064 and
  0065, with every citation updated.
- A failed sign-in says why on the plugin's details, where this branch
  follows it, in dev's words for its code.
- dev's refusal tests keep the plugin waiting to sign in, as this branch
  saves it first, and its failure tests read the plugin's details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:31:35 +05:30
Anish Sarkar
2a8c06c658 feat(plugins): nothing reaches a third-party server while those are off
While third-party plugins are off, every route that reaches a plugin's
server refuses one SurfSense does not publish with 403 third_party_off
before sending anything, and turning the switch off ends any third-party
sign-in under way. Yours says "Third-party plugins are off" with no Connect,
a plugin's details say "Not used" with only Remove, and Add custom plugin
holds Connect beside the same switch and consent as Browse. The in-place
sign-in fallbacks for screens outside Settings › Plugins, which none are,
and the test stand-in's probe route, which the API no longer has, are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:15:40 +05:30
Anish Sarkar
f94e84295a feat(plugins): a plugin's state on one line, why behind an info button
A plugin's details show its state alone, with at most one reason behind an
info button beside it: why it needs an OAuth app, the registry's reason for
a delisted plugin, or "Not used" for a third-party plugin while those are
off. Notes about its tools sit with its tools. The plugin's icon there is as
tall as its name and type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:00:57 +05:30
Anish Sarkar
ecae8de8f6 feat(plugins): one Connect or Disconnect per plugin, and Connect in its row
A plugin's details show one action beside its menu: Connect while it is not
connected, whatever that takes, and Disconnect once it is. Edit connection,
Edit headers, Change OAuth app and License settings move into the menu, and
the status line holds no buttons. Every plugin in Yours not connected, of any
type, shows the same Connect in its row with why beside it in grey; an OAuth
sign-in started there runs in the plugin's details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:34:11 +05:30
Anish Sarkar
265578d6fa feat(plugins): an OAuth plugin is saved first and finished in its details
Connect from Add custom plugin, Browse, a public page or search saves an
OAuth plugin and opens its details in Yours, where the sign-in runs; the
browser opens at once. A provider taking only registered apps, or a
registry entry declaring user_client, keeps the plugin as "Needs an OAuth
app" and its details set one up. A saved Browse entry shows its status as
Yours does, opening its details. A plugin never signed in reads "Not
connected" with Connect, nothing to disconnect, and the user's own OAuth
app is offered only where it is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:09:11 +05:30
Anish Sarkar
dcbb71320e fix(plugins): a server on the older SSE transport signs in with OAuth
A sign-in listed the server's tools over Streamable HTTP alone, so an SSE
server whose POST asks for a sign-in, as Asana's, Atlassian's and
PayPal's /sse do, was signed in and then refused. A finished sign-in now
lists as adding a server does, Streamable HTTP then SSE, and records the
transport that answered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:46:45 +05:30
Anish Sarkar
24bbf29719 fix(plugins): an SSE server's refusal says why, and a retired address is gone
The SSE client raises a refused stream as the HTTP library's error, which
SurfSense's MCP client never kept, so any status an SSE stream answered
was lost. It is now kept, and when neither transport answers, an SSE
address that answered anything but "nothing here" is the one reported: a
key it asks for, a server down, or 410, an address its server retired,
as DeepWiki did its /sse, reported as plugin_gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:29:24 +05:30
Anish Sarkar
948317e7f8 feat(plugins): own OAuth app in three steps, chosen when adding
- The own-app dialog lays out the steps it takes: register an app with
  the provider, give it the callback address, copied in one click with
  a note where a developer build's address moves, and paste the client
  ID and secret, with scopes folded away.
- A plugin's own app is chosen when it is added, from Advanced settings,
  as Claude Code takes it; the menu item on every OAuth plugin is gone.
- A provider whose metadata names no registration, as GitHub's, is asked
  for the user's own app at once, with no registration sent only to
  fail. Refusals SurfSense handles log one line, not the SDK's traceback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:26:23 +05:30
Anish Sarkar
f476926978 feat(plugins): a connection is header rows, as other MCP clients take them
Advanced settings and Edit connection hold rows of a header name and a
hidden value, the first named Authorization with its value hinted
"Bearer your-key", in place of a key with a Bearer, Basic or custom
header switch. Only rows with a value are sent. The switch, its draft
and the Basic encoding are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:25:56 +05:30
Anish Sarkar
b437e0884b feat(plugins): own OAuth app apart, turn notices, tools by message
- The user's own OAuth app has a dialog of its own, opened only when a
  server registers no app or refused SurfSense, from Add or Sign in. A
  plugin signed in with one shows Change OAuth app; the API says which
  with `own_app`. Advanced settings no longer holds it.
- The gateway names a plugin whose refresh is refused in the running
  turn, and answers a call to a tool blocked mid-turn without reaching
  the server. It declines the optional GET stream for Streamable HTTP.
- opencode 1.x fixes a turn's tools when it starts (measured), so its
  budget goes first to the tools the user's message matches.
- Plugin dialogs scroll within a height below Settings', faded.
- The real Claude Code tests serve the API on a port, and one finds a
  deferred plugin tool among 81.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:23:48 +05:30
Anish Sarkar
496e80dc9f feat(agent): size a turn's plugin tools by the window, not 24
A turn offered at most 24 plugin tools, the plugins connected first winning,
so one with many tools hid the rest and the agent called them not connected.

- Claude Code is given every ready tool and finds them with its own tool
  search, pinned with ENABLE_TOOL_SEARCH=auto; SurfSense's server is always
  loaded.
- opencode, which sends every definition with every request, is offered what
  fits a tenth of the model's window: a mentioned plugin first, then each
  plugin a tool in turn.
- A plugin left out is named to the user and to the agent, with how to bring
  it in: a mention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 18:31:08 +05:30
Anish Sarkar
59bd11ac58 feat(agent): runtimes reach plugins through SurfSense's gateway
opencode and Claude Code are given each plugin as a loopback address on the
API and a per-thread key, never its key, headers or token. The gateway checks
the plugin is ready on every request, adds its sign-in, and streams the answer
back.

- An OAuth plugin's 401 mid-session is refreshed once and sent again, so the
  runtime keeps its MCP session.
- A 403 insufficient_scope is recorded as a check records it, and the running
  turn names the plugin as needing more access.
- A server on the older SSE transport has its endpoint pointed back at the
  gateway.
- opencode's proxy now refuses every host, and Claude Code's agent runs use
  the chat proxy; the plugin proxy becomes runtime_proxy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 17:36:09 +05:30
Anish Sarkar
e9b495c0ee docs(plugins): a plugin's connection is a transport and one sign-in
ADR 0062 records the connection model: Streamable HTTP or SSE, found from
the server; no sign-in, headers, or OAuth registered in the spec's order or
as the user's own app; one Add screen with Advanced settings. The plugins
doc describes the built behaviour, and the connections proposal it replaces
is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:30:55 +05:30
Anish Sarkar
b7fa6cfb74 feat(registry): entries name their headers, the user's own app and transport
A registry entry can name the headers a key goes in, say `auth.user_client`
for a provider that takes only registered apps, and record `transport`. The
submission form and the bot check them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 16:29:33 +05:30
Anish Sarkar
d928ec672e docs(plugins): propose connecting any remote MCP server with any sign-in
A connection is a transport, Streamable HTTP or legacy SSE, and one sign-in:
none, headers for any key, or OAuth that registers itself or uses the user's
own app. Detected from the server where it can be, shared by the Add custom
plugin dialog and the registry bot, with no migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 03:50:12 +05:30
Anish Sarkar
d7850f4013 docs(plugins): local MCP servers wait for bundles, not custom plugins
Plugins stay remote MCP servers. A server started by a command is not added
as a custom plugin; local ones come with bundles, whose design is redone
first. The reasons move from the open questions into a decided section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 02:36:31 +05:30
Rohan Verma
1d551c097a
Merge pull request #2299 from MohammadHijjawi97/fix/local-in-stream-cache-full
Some checks failed
Desktop Tests / Changes (push) Has been cancelled
Desktop Tests / Backend (macos-latest) (push) Has been cancelled
Desktop Tests / Backend (ubuntu-latest) (push) Has been cancelled
Desktop Tests / Backend (windows-latest) (push) Has been cancelled
Desktop Tests / Frontend (push) Has been cancelled
Desktop Tests / Electron (macos-latest) (push) Has been cancelled
Desktop Tests / Electron (ubuntu-latest) (push) Has been cancelled
Desktop Tests / Electron (windows-latest) (push) Has been cancelled
Desktop Tests / Desktop Gate (push) Has been cancelled
fix(local): sort llama.cpp's shared-cache overflow as runtime_busy
2026-10-19 12:48:54 -07:00
Anish Sarkar
49e1ab842b feat(local): offer a custom plugin to the registry from its menu
Submit to the registry opens the submission form with the plugin's name and
URL filled in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:36:46 +05:30
Anish Sarkar
4d47e14d4f feat(registry): list a plugin from an issue form a bot checks
A publisher or an agent fills in the Submit a plugin form; no git. The bot
contacts the server as the app does, reads how it signs in and its hosts,
checks the declared tool names against what it lists without sign-in, fetches
the icon, and keeps one comment with the entry or what to fix. A maintainer's
registry: accept label has it open the pull request into dev.

Tool names are required, since the plugin's page shows them; a token entry's
key wording is optional, the app's own being used without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:36:38 +05:30
Rohan Verma
299abf676a
Merge pull request #2287 from MohammadHijjawi97/fix/studio-word-list-numbering
Some checks failed
Desktop Tests / Changes (push) Has been cancelled
Desktop Tests / Backend (macos-latest) (push) Has been cancelled
Desktop Tests / Backend (ubuntu-latest) (push) Has been cancelled
Desktop Tests / Backend (windows-latest) (push) Has been cancelled
Desktop Tests / Frontend (push) Has been cancelled
Desktop Tests / Electron (macos-latest) (push) Has been cancelled
Desktop Tests / Electron (ubuntu-latest) (push) Has been cancelled
Desktop Tests / Electron (windows-latest) (push) Has been cancelled
Desktop Tests / Desktop Gate (push) Has been cancelled
fix(local): count each numbered list in a Studio Word file from 1
2026-10-17 18:28:01 -07:00
Rohan Verma
1263998cd6
Merge pull request #2267 from samuelmbabhazi/fix/plugin-sign-in-reason
fix(plugins): say why a sign-in failed
2026-10-17 18:18:53 -07:00
Rohan Verma
7756caf40c
Merge pull request #2298 from Cedric921/feat/about-data-folder
feat(local): show where SurfSense keeps its data in Settings › About
2026-10-17 18:11:36 -07:00
Rohan Verma
efb156d814
Merge pull request #2265 from Cedric921/ci/release-dry-run-signing
ci(local): keep the macOS certificate off Windows and check Apple's credentials before every build
2026-10-17 18:11:20 -07:00
DESKTOP-RTLN3BA\$punk
6d5be0a1b7 docs(local): ocr/ goes once a scan's text is built, not when the source is ready 2026-10-17 16:09:29 -07:00
Mohammad Hijjawi
d0f3eb2497 fix(local): sort llama.cpp's shared-cache overflow as runtime_busy
At llama.cpp b11050 a unified cache that overflows while decoding ends
every running reply with "Context size has been exceeded."
(server-context.cpp). `_CACHE_FULL` held only the two lines llama.cpp
writes to its log, which no client receives, so:

- before a reply's first token, the 500 was classified
  `model_cannot_run` ("SurfSense cannot run this model. Pick another
  model.");
- after it, the in-stream error raised `StreamedError`, which fell
  through to `unknown`.

Match llama.cpp's own message, and classify a local runtime's
`StreamedError` that carries it as `runtime_busy` too. A hosted
endpoint's in-stream error and other local ones stay `unknown`.
2026-10-17 23:49:40 +01:00
DESKTOP-RTLN3BA\$punk
f3524e786e fix(local): a long OCR reading gives the queue up as a long Docling reading does
Every 8 pages, once the reading has run its minute and an upload or note
waits at its priority, OCR keeps the pages read so far in ocr/pages.json and
gives way; the run that resumes reads only the pages left. It gives way only
with the pages on disk, and never after its last page.

A waiting source with ocr/ counts as a reading that gave way itself, so two
long scans do not take turns. ocr/ goes with a cancel or a failure, in OCR's
part or in Docling's after it, when Docling takes the file over, and when a
cancel lands while it waits in the queue, so a retried source is never taken
for one that gave way.
2026-10-17 15:29:58 -07:00
Cedric Karungu
e59522081b feat(local): show where SurfSense keeps its data in Settings › About 2026-10-17 23:34:03 +02:00
DESKTOP-RTLN3BA\$punk
53f555a074 Merge remote-tracking branch 'origin/dev' into perf/os-ocr
# Conflicts:
#	docs/adr/README.md
#	surfsense_local/backend/pyproject.toml
2026-10-17 14:21:38 -07:00
Cedric Karungu
1937f77949 docs(proposals): give the theme provider a resolved theme for Mermaid 2026-10-17 21:50:09 +02:00
Cedric Karungu
788549f43a docs(proposals): render Mermaid diagrams in the desktop app 2026-10-17 21:13:55 +02:00
Anish Sarkar
8178ed701c fix(local): a thread opens while the engine it left cannot answer
A thread that moved from opencode to Claude Code keeps its opencode session,
and reading it failed whole when opencode was not running. The engine the
thread left is now left out of the read, with a warning; the engine running
it is never skipped. Both readers take the engines as an argument and pick
the current one from the thread's agent_engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:32:49 +05:30
Anish Sarkar
e2c1d2a719 docs(local): a thread's engine moves with the selected model; claude_plan and revisions to 0032
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:32:46 +05:30
DESKTOP-RTLN3BA\$punk
52556ca2ec fix(local): the scan OCR check needs the sample's words, and an image with no frame of text goes to Docling 2026-10-17 03:47:02 -07:00
DESKTOP-RTLN3BA\$punk
02f88cc121 docs(local): ADR 0060 and documents.md with the end-to-end and Linux timings 2026-10-17 02:57:07 -07:00
DESKTOP-RTLN3BA\$punk
6e07853d38 fix(local): scan OCR review fixes: close renders, one cap, upright turns, resume, figures on ask
Renders close on the ingest thread whatever stops a reading; pages and a
sideways page's turns share one cap; a turn replaces a page only when it
reads as text, and lines standing on end count as sideways; a cover keeps
its own title over OCR's scraps; blank pages alone send the file to
Docling; OCR's pages are kept while Docling's part of the file gives the
queue up; a scan's figures wait to be asked; no process-wide onnxruntime
patch.
2026-10-17 02:03:39 -07:00
Anish Sarkar
af5f45e7d2 fix(local): refuse a Claude Code too old for the plan before it runs, and say so above the composer
- One gatekeeper, usable_claude(), decides whether Claude Code may run: found,
  and not older than MIN_VERSION (an unreadable version is let through). The
  chat model's check, a chat, title or Studio run, and an Agentic turn all ask
  it before anything streams.
- The version is read once per binary as installed (path and modified time),
  shared with the newer-than-tested log warning.
- A send refused as claude_too_old or claude_sign_in_required (409, answered
  app-wide) launches nothing: the message goes back to the composer and the
  chat model's check runs again, so its notice says why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:14:42 +05:30
Anish Sarkar
7f030d5f07 fix(local): a Claude plan reply on extra usage goes on instead of stopping at the plan's limit
Past the plan's limit Claude Code reports the plan as rejected even while
extra usage pays (isUsingOverage, overageInUse), and goes on answering.
SurfSense took every rejected event as the limit and ended the reply. It now
applies Claude Code 2.1.301's own rule: only a refusal with no extra usage
ends a reply, and the composer notice follows either field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:06:43 +05:30
Anish Sarkar
83f7b0d08a fix(local): close more Claude plan gaps and say above the composer when the plan is on extra usage
- Claude Code's own skills are refused by its skills allowlist, measured; the
  doc says so instead of listing it as a gap. A session resumed after many
  engine switches is stated as a choice, not a gap.
- A Claude Code newer than the newest the claude_code tests passed on is named
  once in the log; NEWEST_TESTED is raised only once they pass again.
- Claude Code's transcript is deleted after each agent turn and its saved
  results kept; deleting a thread removes both without delete_session.
- Each Claude plan reply sends where the plan stands (plan-extra-usage); while
  it is on extra usage and a Claude plan model is selected, the composer says
  so until a reply comes back within the plan or the plan resets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:57:53 +05:30
Anish Sarkar
3c3e73ba92 fix(local): close three Claude plan gaps and delete every engine's session with its thread
- A plugin whose offered tools changed is reconnected in the running Claude
  Code (reconnect_mcp_server) instead of keeping its old listing; a failed
  reconnect starts the client over.
- A thread's Claude session is resumed only under the account that made it:
  chat_threads.claude_account (revision 0032) keeps a digest of the account,
  and another account gets a new session told the whole thread. The thread
  reads back every session it had.
- One test runs the same scenarios through opencode and Claude Code and
  compares what the chat shows and reads back.
- Deleting a thread or workspace deletes the session of every engine that ran
  the thread, not only the one it is on now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:10:14 +05:30
Anish Sarkar
6bdbb969d3 docs(local): the Claude plan is architecture now, not a proposal
The work the Claude plan proposal laid out is built, so what is true now
moves to docs/architecture/claude-plan.md and the proposal is deleted, as
the docs process asks of the change that completes the work. What the
proposal promised and the code does not do yet is listed as Known gaps.
ADR 0057 says where the code stands, and the docs and code comments that
cited the proposal cite the architecture doc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:41:26 +05:30
Anish Sarkar
5cbbb06c4d fix(local): a Claude plan turn reads a large tool result Claude Code saved for it
Claude Code saves a tool result too large to give whole under the
session's tool-results folder and tells the model to read it there,
which the gate refused as outside the thread. The gate now lets the model
read that session's saved results alone, as opencode's rules leave its
tool-output folder open; another session's, Claude Code's settings and
any write stay refused. The comment and docs that said a raised
MAX_MCP_OUTPUT_TOKENS stops results being saved are corrected, and the
proposal notes that the tests running the real Claude Code will go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 03:39:16 +05:30
Anish Sarkar
8baa1865d5 fix(local): a Claude plan turn names a plugin unavailable only once Claude Code has tried it
The Claude turn asked Claude Code for its plugins' status right after it
started, when every one is still pending, and named them all unreachable.
It now reads them from the turn's init message, which Claude Code holds
until each has connected or failed. Both engines follow one rule: a
plugin is unavailable only from its engine's settled answer, within the
same 25 seconds, now Claude Code's MCP_TIMEOUT too, and for the same
three reasons, which move out of opencode's own module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:46:52 +05:30
Anish Sarkar
fd171c873f Merge remote-tracking branch 'upstream/dev' into feat/claude-plan 2026-10-17 02:22:00 +05:30
Anish Sarkar
26734a5d30 fix(local): tunnels through SurfSense's proxy work under uvloop
The API runs on uvloop on macOS and Linux, whose create_connection has
no happy_eyeballs_delay, so every tunnel failed there and opencode never
reached a plugin. The option is now passed only on asyncio's own loop,
which the API runs on Windows, where it was added. Any failed connect is
answered 502 rather than closed unanswered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:58:24 +05:30
Anish Sarkar
8ce2b6c14d fix(local): new tools show as a badge beside On, not in place of it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:25:46 +05:30
Anish Sarkar
b810d57b45 fix(local): tunnels through SurfSense's proxy work under uvloop
The API runs on uvloop on macOS and Linux, whose create_connection has
no happy_eyeballs_delay, so every tunnel failed there: Claude Code never
reached Anthropic, and opencode never reached a plugin. The option is now
passed only on asyncio's own loop, which the API runs on Windows, where
it was added. Any failed connect is answered 502 rather than closed, which
Claude Code would retry ten times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:52:26 +05:30
Anish Sarkar
9d2ea9c59a fix(local): the Claude plan row shows at once, and the dialog says to come back
The row waited on an endpoint that runs the user's claude twice. It is
split: GET /llm/connections/claude says what a plan fills and the hosts
still off without running Claude Code, and GET .../claude/status reads
Claude Code's state live, for the dialog alone. With Claude Code not
installed, the dialog now ends its steps with coming back and checking
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:29:30 +05:30
Anish Sarkar
cfc2a3a6c2 feat(local): Agentic threads run on Claude Code for a Claude plan model
A Claude plan model now opens Agentic threads, with no opencode needed,
and its turns run on the user's own Claude Code with what opencode's do:
SurfSense's tools over the same endpoint, the thread's plugins, skills,
the folder rules, approvals, Stop, and the same frames on screen. A
running Claude Code is kept per thread and relaunched when a turn's
servers, rules or model change; an interrupted turn is read to its end
so it never answers the next message.

Changing models across engines hands the thread over: the engine taking
it resumes its own session, or opens one in the thread's folder, and is
given a note of the turns it missed; the thread reads back as one.

The tool, permission and step routes are mounted without opencode too,
behind the launch key, and a Claude thread is offered the tools that
return images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:58:41 +05:30